diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..f42fd438 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +axiosInstance.tsx \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..75d97586 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 00000000..3bc60ed5 --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..03d9549e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..9d3a0203 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 00000000..b0c1c68f --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/sakai-react.iml b/.idea/sakai-react.iml new file mode 100644 index 00000000..24643cc3 --- /dev/null +++ b/.idea/sakai-react.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 00000000..42014037 --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/webServers.xml b/.idea/webServers.xml new file mode 100644 index 00000000..083d91ec --- /dev/null +++ b/.idea/webServers.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/(full-page)/auth/access/page.tsx b/app/(full-page)/auth/access/page.tsx deleted file mode 100644 index 3315af64..00000000 --- a/app/(full-page)/auth/access/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -'use client'; -import { useRouter } from 'next/navigation'; -import React from 'react'; -import { Button } from 'primereact/button'; - -const AccessDeniedPage = () => { - const router = useRouter(); - - return ( -
-
- Sakai logo -
-
-
- -
-

Access Denied

-
You do not have the necessary permisions.
- Error -
-
-
-
- ); -}; - -export default AccessDeniedPage; diff --git a/app/(full-page)/auth/error/page.tsx b/app/(full-page)/auth/error/page.tsx deleted file mode 100644 index bca51f31..00000000 --- a/app/(full-page)/auth/error/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -'use client'; -import { useRouter } from 'next/navigation'; -import React from 'react'; -import { Button } from 'primereact/button'; - -const ErrorPage = () => { - const router = useRouter(); - - return ( -
-
- Sakai logo -
-
-
- -
-

Error Occured

-
Something went wrong.
- Error -
-
-
-
- ); -}; - -export default ErrorPage; diff --git a/app/(full-page)/auth/login/page.tsx b/app/(full-page)/auth/login/page.tsx index d3dd85ca..e8d6e759 100644 --- a/app/(full-page)/auth/login/page.tsx +++ b/app/(full-page)/auth/login/page.tsx @@ -1,65 +1,340 @@ /* eslint-disable @next/next/no-img-element */ 'use client'; -import { useRouter } from 'next/navigation'; -import React, { useContext, useState } from 'react'; -import { Checkbox } from 'primereact/checkbox'; -import { Button } from 'primereact/button'; -import { Password } from 'primereact/password'; +import React, { useContext, useEffect, useState } from 'react'; import { LayoutContext } from '../../../../layout/context/layoutcontext'; import { InputText } from 'primereact/inputtext'; -import { classNames } from 'primereact/utils'; + +import { useForm } from 'react-hook-form'; +import { schema } from '@/schemas/authSchema'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Controller } from 'react-hook-form'; +import { getState, getUser, login, send, verifyTelegram } from '@/services/auth'; +import FancyLinkBtn from '@/app/components/buttons/FancyLinkBtn'; +import { LoginType } from '@/types/login'; +import Link from 'next/link'; +import { Button } from 'primereact/button'; +import { getToken } from '@/utils/auth'; +import { ProgressSpinner } from 'primereact/progressspinner'; + +import { useLocalization } from '../../../../layout/context/localizationcontext'; +import { useSearchParams } from 'next/navigation'; +import { Dialog } from 'primereact/dialog'; + +interface State { + status: number; + message: string; +} const LoginPage = () => { - const [password, setPassword] = useState(''); - const [checked, setChecked] = useState(false); - const { layoutConfig } = useContext(LayoutContext); + const params = useSearchParams(); + const backRedirect = params.get('redirect'); + const { translations } = useLocalization(); + const { setUser, setMessage, setDepartament } = useContext(LayoutContext); + const [showPassword, setShowPassword] = useState(false); + const [progressSpinner, setProgressSpinner] = useState(false); + const [disabledState, setDisabledState] = useState(false); + const [visible, setVisible] = useState(false); + const [code, setCode] = useState(''); + const [expiresAt, setExpiresAt] = useState(''); + const [timeLeft, setTimeLeft] = useState({ minutes: 0, seconds: 0 }); + const [token, setToken] = useState(''); + const [verifyBtnDisabled, setVerifyBtnDisabled] = useState(true); + + const { + register, + handleSubmit, + formState: { errors }, + control + } = useForm({ + resolver: yupResolver(schema), + mode: 'onChange' + }); + + const handleGetState = async () => { + const data: State = await getState(); + return data; + }; + + const handleSend = async () => { + const data: State = await send(); + return data; + }; + + const handleConfirm = () => { + setVisible(true); + }; + + const handleVerifyClick = () => { + handleVerify(code); + }; + + const handleVerify = async (enteredCode: string) => { + const data = await verifyTelegram(enteredCode); + setCode(''); + if (data?.status) { + setCode(''); + setMessage({ state: true, value: { severity: 'error', summary: 'Ошибка при подтверждение кода', detail: 'Проверьте правильность ввода и срок действия кода' } }); + } else { + setVisible(false); + userGetSection(null); + } + }; + + const userGetSection = async (tokenParam: string | null) => { + const localToken = tokenParam ? tokenParam : token; + if (localToken) { + console.log('ready'); + const res = await getUser(); + try { + if (res?.success) { + if (!res?.user.is_working && !res?.user.is_student) { + setMessage({ + state: true, + value: { + severity: 'error', + summary: 'Не удалось определить ваш статус пользователя.', + detail: ( +
+ Обратитесь в службу поддержки, указав необходимые данные {res?.user?.myedu_id} +
+ ) + } + }); + document.cookie = 'access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;'; + } else { + if (res?.user.is_working) { + if (res.roles && res.roles.length > 0) { + const roleCheck = res.roles.find((i: { id_role: number }) => i.id_role); + if (roleCheck) { + setDepartament({ info: roleCheck.roles_name.info_ru, last_name: res.user?.last_name, name: res?.user.name, father_name: res.user?.father_name }); + } + let safeRedirect = '/dashboard'; + if (backRedirect && backRedirect.startsWith('/')) { + safeRedirect = backRedirect; + } + + window.location.href = safeRedirect; + } else { + let safeRedirect = '/dashboard'; + if (backRedirect && backRedirect.startsWith('/')) { + safeRedirect = backRedirect; + } + + window.location.href = safeRedirect; + } + } + if (res?.user.is_student) { + let safeRedirect = '/studentHome'; + if (backRedirect && backRedirect.startsWith('/')) { + safeRedirect = backRedirect; + } + window.location.href = safeRedirect; + } + } + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при авторизации', detail: 'Повторите позже' } + }); // messege - Ошибка при авторизации, повторите позже + setUser(null); + localStorage.removeItem('userVisit'); + document.cookie = 'access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;'; + console.log('Ошибка при получении пользователя'); + } + } catch (error) { + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при авторизации', detail: 'Повторите позже' } + }); // messege - Ошибка при авторизации, повторите позже + + setUser(null); + localStorage.removeItem('userVisit'); + document.cookie = 'access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;'; + console.log('Ошибка при получении пользователя'); + } + } else { + console.log('stop'); + } + }; + + const getRemainingTime = (expiresAt: string) => { + const now = Date.now(); + const expire = new Date(expiresAt).getTime(); + + const diff = expire - now; + + if (diff <= 0) { + return { minutes: 0, seconds: 0 }; + } - const router = useRouter(); - const containerClassName = classNames('surface-ground flex align-items-center justify-content-center min-h-screen min-w-screen overflow-hidden', { 'p-input-filled': layoutConfig.inputStyle === 'filled' }); + const minutes = Math.floor(diff / 1000 / 60); + const seconds = Math.floor((diff / 1000) % 60); + + return { minutes, seconds }; + }; + + const formatTime = (value: number) => { + return value < 10 ? `0${value}` : value; + }; + + const onSubmit = async (value: LoginType) => { + try { + setDisabledState(true); + const user = await login(value); + if (user && user?.success) { + document.cookie = `access_token=${user.token.access_token}; path=/; Secure; SameSite=Strict; expires=${user.token.expires_at}`; + const token = user.token.access_token; + setToken(token); + const state: any = await handleGetState(); + const fa = '2fa'; + // console.log(state); + // console.log(fa); + // console.log(state[fa]); + if (state[fa]) { + userGetSection(token); + } else if (state && state?.sendCode) { + const send: any = await handleSend(); + if (send && send?.token) { + setExpiresAt(send?.expired); + handleConfirm(); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при авторизации', detail: 'Повторите позже' } + }); + } + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при авторизации', detail: 'Повторите позже' } + }); + } + } else { + if (user?.status === 401 && user?.response?.data?.redirect_url) { + window.location.href = user?.response?.data?.redirect_url; + } + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при авторизации', detail: 'Повторите позже' } + }); // messege - Ошибка при авторизации при авторизации + } + } catch (err) { + console.error('Критическая ошибка в onSubmit:', err); + } finally { + setTimeout(() => { + setDisabledState(false); + }, 2000); + } + }; + + const onError = (errors: any) => { + console.log('Ошибки формы:', errors); + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при авторизации', detail: 'Введите корректные данные' } + }); + }; + + useEffect(() => { + const token = getToken('access_token'); + if (token) { + // window.location.href = '/'; + setProgressSpinner(false); + } else { + setProgressSpinner(false); + } + }, []); + + useEffect(() => { + if (!visible || !expiresAt) return; + + const interval = setInterval(() => { + const remaining = getRemainingTime(expiresAt); + setTimeLeft(remaining); + + if (remaining.minutes === 0 && remaining.seconds === 0) { + clearInterval(interval); + setVisible(false); // 👈 закрываем окно + setExpiresAt(null); // очищаем + setCode(''); // очищаем код + } + }, 1000); + + return () => clearInterval(interval); + }, [visible, expiresAt]); + + useEffect(() => { + if (code?.length > 5) setVerifyBtnDisabled(false); + else setVerifyBtnDisabled(true); + }, [code]); + + if (progressSpinner) + return ( +
+ +
+ ); return ( -
-
- Sakai logo -
-
-
- Image -
Welcome, Isabel!
- Sign in to continue +
+ {/*
*/} + {/* */} +
+ setVisible(false)}> +
+
+
- -
- - - - - setPassword(e.target.value)} placeholder="Password" toggleMask className="w-full mb-5" inputClassName="w-full p-3 md:w-30rem"> - -
-
- setChecked(e.checked ?? false)} className="mr-2"> - -
- - Forgot password? - -
- +

{'Введите код с телеграмм'}

+
+ {'Код действителен до'}:{' '} + + {formatTime(timeLeft.minutes)}:{formatTime(timeLeft.seconds)} +
+ setCode(e.target.value)} placeholder={'Введите код'} className="w-full p-inputtext-md text-center" /> + +
+
+
+

{translations.login}

+
+
+ + {errors.email && {errors.email.message}} +
+
+ {/* } + /> */} + ( +
+ +
+ )} + /> + {errors.password && {errors.password.message}} +
+ +
+ +
+
+ + +
+ {/**/}
); }; diff --git a/app/(full-page)/landing/page.tsx b/app/(full-page)/landing/page.tsx deleted file mode 100644 index 9377d188..00000000 --- a/app/(full-page)/landing/page.tsx +++ /dev/null @@ -1,557 +0,0 @@ -'use client'; -/* eslint-disable @next/next/no-img-element */ -import React, { useContext, useRef, useState } from 'react'; -import Link from 'next/link'; - -import { StyleClass } from 'primereact/styleclass'; -import { Button } from 'primereact/button'; -import { Ripple } from 'primereact/ripple'; -import { Divider } from 'primereact/divider'; -import { LayoutContext } from '../../../layout/context/layoutcontext'; -import { NodeRef } from '@/types'; -import { classNames } from 'primereact/utils'; - -const LandingPage = () => { - const [isHidden, setIsHidden] = useState(false); - const { layoutConfig } = useContext(LayoutContext); - const menuRef = useRef(null); - - const toggleMenuItemClick = () => { - setIsHidden((prevState) => !prevState); - }; - - return ( -
-
-
- - Sakai Logo - SAKAI - - - - - -
- -
-
-

- Eu sem integereget magna fermentum -

-

Sed blandit libero volutpat sed cras. Fames ac turpis egestas integer. Placerat in egestas erat...

- -
-
- Hero Image -
-
- -
-
-
-

Marvelous Features

- Placerat in egestas erat... -
- -
-
-
-
- -
-
Easy to Use
- Posuere morbi leo urna molestie. -
-
-
- -
-
-
-
- -
-
Fresh Design
- Semper risus in hendrerit. -
-
-
- -
-
-
-
- -
-
Well Documented
- Non arcu risus quis varius quam quisque. -
-
-
- -
-
-
-
- -
-
Responsive Layout
- Nulla malesuada pellentesque elit. -
-
-
- -
-
-
-
- -
-
Clean Code
- Condimentum lacinia quis vel eros. -
-
-
- -
-
-
-
- -
-
Dark Mode
- Convallis tellus id interdum velit laoreet. -
-
-
- -
-
-
-
- -
-
Ready to Use
- Mauris sit amet massa vitae. -
-
-
- -
-
-
-
- -
-
Modern Practices
- Elementum nibh tellus molestie nunc non. -
-
-
- -
-
-
-
- -
-
Privacy
- Neque egestas congue quisque. -
-
-
- -
-
-

Joséphine Miller

- Peak Interactive -

- “Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est - laborum.” -

- Company logo -
-
-
-
- -
-
-

Powerful Everywhere

- Amet consectetur adipiscing elit... -
- -
-
- mockup mobile -
- -
-
- -
-

Congue Quisque Egestas

- - Lectus arcu bibendum at varius vel pharetra vel turpis nunc. Eget aliquet nibh praesent tristique magna sit amet purus gravida. Sit amet mattis vulputate enim nulla aliquet. - -
-
- -
-
-
- -
-

Celerisque Eu Ultrices

- - Adipiscing commodo elit at imperdiet dui. Viverra nibh cras pulvinar mattis nunc sed blandit libero. Suspendisse in est ante in. Mauris pharetra et ultrices neque ornare aenean euismod elementum nisi. - -
- -
- mockup -
-
-
- -
-
-

Matchless Pricing

- Amet consectetur adipiscing elit... -
- -
-
-
-

Free

- free -
- $0 - per month - -
- -
    -
  • - - Responsive Layout -
  • -
  • - - Unlimited Push Messages -
  • -
  • - - 50 Support Ticket -
  • -
  • - - Free Shipping -
  • -
-
-
- -
-
-

Startup

- startup -
- $1 - per month - -
- -
    -
  • - - Responsive Layout -
  • -
  • - - Unlimited Push Messages -
  • -
  • - - 50 Support Ticket -
  • -
  • - - Free Shipping -
  • -
-
-
- -
-
-

Enterprise

- enterprise -
- $999 - per month - -
- -
    -
  • - - Responsive Layout -
  • -
  • - - Unlimited Push Messages -
  • -
  • - - 50 Support Ticket -
  • -
  • - - Free Shipping -
  • -
-
-
-
-
- -
-
-
- - footer sections - SAKAI - -
- -
-
- - -
-

Resources

- Get Started - Learn - Case Studies -
- -
-

Community

- Discord - - Events - badge - - FAQ - Blog -
- - -
-
-
-
-
-
- ); -}; - -export default LandingPage; diff --git a/app/(full-page)/layout.tsx b/app/(full-page)/layout.tsx index 7d5747fe..4f4a0497 100644 --- a/app/(full-page)/layout.tsx +++ b/app/(full-page)/layout.tsx @@ -7,7 +7,7 @@ interface SimpleLayoutProps { } export const metadata: Metadata = { - title: 'PrimeReact Sakai', + title: 'Mooc ОшГУ', description: 'The ultimate collection of design-agnostic, flexible and accessible React UI Components.' }; @@ -15,7 +15,7 @@ export default function SimpleLayout({ children }: SimpleLayoutProps) { return ( {children} - + {/* */} ); } diff --git a/app/(full-page)/pages/notfound/page.tsx b/app/(full-page)/pages/notfound/page.tsx index 52c0b4ce..c9c4dc0e 100644 --- a/app/(full-page)/pages/notfound/page.tsx +++ b/app/(full-page)/pages/notfound/page.tsx @@ -49,6 +49,6 @@ const NotFoundPage = () => {
); -}; +}; export default NotFoundPage; diff --git a/app/(main)/archive/page.tsx b/app/(main)/archive/page.tsx new file mode 100644 index 00000000..a9228f3a --- /dev/null +++ b/app/(main)/archive/page.tsx @@ -0,0 +1,163 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import { Button } from 'primereact/button'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { Tag } from 'primereact/tag'; +import { DataView } from 'primereact/dataview'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import SubTitle from '@/app/components/titles/SubTitle'; +import { fetchArchivedCourses } from '@/services/courses'; +import { myMainCourseType } from '@/types/myMainCourseType'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +interface ArchivedCourse extends myMainCourseType { + archive_course: { + copy_course_id: number; + copy_have: boolean; + course_id: number; + id: number; + created_at: string; + }; + archived: boolean; +} + +const ArchivePage = () => { + const [archivedCourses, setArchivedCourses] = useState([]); + const [loading, setLoading] = useState(true); + const [expandedRows, setExpandedRows] = useState(null); + const isMobile = useMediaQuery('(max-width: 768px)'); + const { translations } = useLocalization(); + + // Обновленная функция запроса с новыми моковыми данными + const handleFetchArchivedCourses = async () => { + setLoading(true); + const data = await fetchArchivedCourses(); + if (data?.success) { + setArchivedCourses(data.courses); + } else { + setArchivedCourses([]); + } + setLoading(false); + }; + + useEffect(() => { + handleFetchArchivedCourses(); + }, []); + + // Шаблоны для колонок DataTable + const imageBodyTemplate = (rowData: ArchivedCourse) => {rowData.title}; + + const publishedBodyTemplate = (rowData: ArchivedCourse) => (rowData.archive_course.copy_have ? : ); + + // Шаблон для мобильного вида DataView + const itemTemplate = (course: ArchivedCourse) => { + return ( +
+
+
+
+ {course.title} +
+
+
{course.title}
+
{translations.archiveDate}: {new Date(course?.archive_course?.created_at).toLocaleDateString()}
+
+
+ {translations.copy}: + {publishedBodyTemplate(course)} +
+ {/*
*/} + {/* Балл: */} + {/* {course.max_score} */} + {/*
*/} + {/*
*/} + {/* Потоки: */} + {/* {course.streams_count} */} + {/*
*/} +
+ {/*
+
+ На рассмотрении: + {reviewBodyTemplate(course)} +
+
+ Публикация: + {publishedBodyTemplate(course)} +
+
*/} + {/*
+
+ {/* {expandedRows && expandedRows[course.id] && rowExpansionTemplate(course)} */} +
+
+ ); + }; + + const header = ; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ +
+

{translations.readOnly}

+

{translations.archiveReadOnlyNotice}

+
+
+
+ + {isMobile ? ( + + ) : ( + setExpandedRows(e.data)} + // rowExpansionTemplate={rowExpansionTemplate} + loading={loading} + emptyMessage="..." + className="p-datatable-striped filter transition-all duration-300 text-sm " + removableSort + rows={5} + > + {/* */} + rowIndex + 1} header={translations.numberSign} style={{ width: '20px' }}> + + + + new Date(rowData?.archive_course?.created_at).toLocaleDateString()} /> + {/* + + + */} + + )} +
+ ); +}; + +export default ArchivePage; + diff --git a/app/(main)/blocks/page.tsx b/app/(main)/blocks/page.tsx deleted file mode 100644 index ebd5a25c..00000000 --- a/app/(main)/blocks/page.tsx +++ /dev/null @@ -1,836 +0,0 @@ -'use client'; -import React, { useState } from 'react'; - -import { InputText } from 'primereact/inputtext'; -import { Chip } from 'primereact/chip'; -import { Checkbox } from 'primereact/checkbox'; -import { Button } from 'primereact/button'; -import BlockViewer from '../../../demo/components/BlockViewer'; - -const Free = () => { - const [checked, setChecked] = useState(false); - - const block1 = ` -
-
-
- Create the screens -
your visitors deserve to see
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

- -
-
-
- hero-1 -
-
- `; - - const block2 = ` -
-
- One Product, - Many Solutions -
-
Ac turpis egestas maecenas pharetra convallis posuere morbi leo urna.
-
-
- - - -
Built for Developers
- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. -
-
- - - -
End-to-End Encryption
- Risus nec feugiat in fermentum posuere urna nec. Posuere sollicitudin aliquam ultrices sagittis. -
-
- - - -
Easy to Use
- Ornare suspendisse sed nisi lacus sed viverra tellus. Neque volutpat ac tincidunt vitae semper. -
-
- - - -
Fast & Global Support
- Fermentum et sollicitudin ac orci phasellus egestas tellus rutrum tellus. -
-
- - - -
Open Source
- Nec tincidunt praesent semper feugiat. Sed adipiscing diam donec adipiscing tristique risus nec feugiat. -
-
- - - -
Trusted Securitty
- Mattis rhoncus urna neque viverra justo nec ultrices. Id cursus metus aliquam eleifend. -
-
-
- `; - - const block3 = ` -
-
Pricing Plans
-
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Velit numquam eligendi quos.
- -
-
-
-
-
Basic
-
Plan description
-
-
- $9 - per month -
-
-
    -
  • - - Arcu vitae elementum -
  • -
  • - - Dui faucibus in ornare -
  • -
  • - - Morbi tincidunt augue -
  • -
-
-
-
-
- -
-
-
-
Premium
-
Plan description
-
-
- $29 - per month -
-
-
    -
  • - - Arcu vitae elementum -
  • -
  • - - Dui faucibus in ornare -
  • -
  • - - Morbi tincidunt augue -
  • -
  • - - Duis ultricies lacus sed -
  • -
-
-
-
-
- -
-
-
-
Enterprise
-
Plan description
-
-
- $49 - per month -
-
-
    -
  • - - Arcu vitae elementum -
  • -
  • - - Dui faucibus in ornare -
  • -
  • - - Morbi tincidunt augue -
  • -
  • - - Duis ultricies lacus sed -
  • -
  • - - Imperdiet proin -
  • -
  • - - Nisi scelerisque -
  • -
-
-
-
-
-
-
- `; - - const block4 = ` -
-
 POWERED BY DISCORD
-
Join Our Design Community
-
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Velit numquam eligendi quos.
-
- `; - - const block5 = ` -
-
🔥 Hot Deals!
-
- Libero voluptatum atque exercitationem praesentium provident odit. -
- - Learn More - - - - -
- `; - - const block6 = ` -
- -
-
-
Customers
-
-
- - 332 Active Users -
-
- - 9402 Sessions -
-
- - 2.32m Avg. Duration -
-
-
-
-
-
-
- `; - - const block7 = ` -
-
-
-
-
- Orders -
152
-
-
- -
-
- 24 new - since last visit -
-
-
-
-
-
- Revenue -
$2.100
-
-
- -
-
- %52+ - since last week -
-
-
-
-
-
- Customers -
28441
-
-
- -
-
- 520 - newly registered -
-
-
-
-
-
- Comments -
152 Unread
-
-
- -
-
- 85 - responded -
-
-
- `; - - const block8 = ` -
-
-
- hyper -
Welcome Back
- Don't have an account? - Create today! -
- -
- - - - - - -
-
- setChecked(e.checked)} checked={checked} className="mr-2" /> - -
- Forgot your password? -
- -
-
-
- `; - - const block9 = ` -
-
Movie Information
-
Morbi tristique blandit turpis. In viverra ligula id nulla hendrerit rutrum.
-
    -
  • -
    Title
    -
    Heat
    -
    -
    -
  • -
  • -
    Genre
    -
    - - - -
    -
    -
    -
  • -
  • -
    Director
    -
    Michael Mann
    -
    -
    -
  • -
  • -
    Actors
    -
    Robert De Niro, Al Pacino
    -
    -
    -
  • -
  • -
    Plot
    -
    - A group of professional bank robbers start to feel the heat from police - when they unknowingly leave a clue at their latest heist.
    -
    -
    -
  • -
-
- `; - - const block10 = ` -
-
Card Title
-
Vivamus id nisl interdum, blandit augue sit amet, eleifend mi.
-
-
- `; - - return ( - <> - -
-
-
- Create the screens -
your visitors deserve to see
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

- -
-
-
- hero-1 -
-
-
- - -
-
- One Product, - Many Solutions -
-
Ac turpis egestas maecenas pharetra convallis posuere morbi leo urna.
-
-
- - - -
Built for Developers
- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. -
-
- - - -
End-to-End Encryption
- Risus nec feugiat in fermentum posuere urna nec. Posuere sollicitudin aliquam ultrices sagittis. -
-
- - - -
Easy to Use
- Ornare suspendisse sed nisi lacus sed viverra tellus. Neque volutpat ac tincidunt vitae semper. -
-
- - - -
Fast & Global Support
- Fermentum et sollicitudin ac orci phasellus egestas tellus rutrum tellus. -
-
- - - -
Open Source
- Nec tincidunt praesent semper feugiat. Sed adipiscing diam donec adipiscing tristique risus nec feugiat. -
-
- - - -
Trusted Securitty
- Mattis rhoncus urna neque viverra justo nec ultrices. Id cursus metus aliquam eleifend. -
-
-
-
- - -
-
Pricing Plans
-
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Velit numquam eligendi quos.
- -
-
-
-
-
Basic
-
Plan description
-
-
- $9 - per month -
-
-
    -
  • - - Arcu vitae elementum -
  • -
  • - - Dui faucibus in ornare -
  • -
  • - - Morbi tincidunt augue -
  • -
-
-
-
-
- -
-
-
-
Premium
-
Plan description
-
-
- $29 - per month -
-
-
    -
  • - - Arcu vitae elementum -
  • -
  • - - Dui faucibus in ornare -
  • -
  • - - Morbi tincidunt augue -
  • -
  • - - Duis ultricies lacus sed -
  • -
-
-
-
-
- -
-
-
-
Enterprise
-
Plan description
-
-
- $49 - per month -
-
-
    -
  • - - Arcu vitae elementum -
  • -
  • - - Dui faucibus in ornare -
  • -
  • - - Morbi tincidunt augue -
  • -
  • - - Duis ultricies lacus sed -
  • -
  • - - Imperdiet proin -
  • -
  • - - Nisi scelerisque -
  • -
-
-
-
-
-
-
-
- - -
-
-  POWERED BY DISCORD -
-
Join Our Design Community
-
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Velit numquam eligendi quos.
-
-
- - -
-
🔥 Hot Deals!
-
- Libero voluptatum atque exercitationem praesentium provident odit. -
- - Learn More - - - - -
-
- - -
- -
-
-
Customers
-
-
- - 332 Active Users -
-
- - 9402 Sessions -
-
- - 2.32m Avg. Duration -
-
-
-
-
-
-
-
- - -
-
-
-
-
- Orders -
152
-
-
- -
-
- 24 new - since last visit -
-
-
-
-
-
- Revenue -
$2.100
-
-
- -
-
- %52+ - since last week -
-
-
-
-
-
- Customers -
28441
-
-
- -
-
- 520 - newly registered -
-
-
-
-
-
- Comments -
152 Unread
-
-
- -
-
- 85 - responded -
-
-
-
- - -
-
-
- hyper -
Welcome Back
- Do not have an account? - Create today! -
- -
- - - - - - -
-
- setChecked(e.checked as boolean)} checked={checked} className="mr-2" /> - -
- Forgot your password? -
- -
-
-
-
- - -
-
Movie Information
-
Morbi tristique blandit turpis. In viverra ligula id nulla hendrerit rutrum.
-
    -
  • -
    Title
    -
    Heat
    -
    -
    -
  • -
  • -
    Genre
    -
    - - - -
    -
    -
    -
  • -
  • -
    Director
    -
    Michael Mann
    -
    -
    -
  • -
  • -
    Actors
    -
    Robert De Niro, Al Pacino
    -
    -
    -
  • -
  • -
    Plot
    -
    A group of professional bank robbers start to feel the heat from police when they unknowingly leave a clue at their latest heist.
    -
    -
    -
  • -
-
-
- - -
-
Card Title
-
Vivamus id nisl interdum, blandit augue sit amet, eleifend mi.
-
-
-
- - ); -}; - -export default Free; diff --git a/app/(main)/course/[page]/page.tsx b/app/(main)/course/[page]/page.tsx new file mode 100644 index 00000000..48ae0923 --- /dev/null +++ b/app/(main)/course/[page]/page.tsx @@ -0,0 +1,1242 @@ +'use client'; + +import FormModal from '@/app/components/popUp/FormModal'; +import { addCourse, addOpenTypes, archiveCourse, deleteCourse, fetchCourseInfo, fetchCourseOpenStatus, fetchCourses, updateCourse, veryfyCourse } from '@/services/courses'; +import { Button } from 'primereact/button'; +import { FileUpload, FileUploadSelectEvent } from 'primereact/fileupload'; +import { InputText } from 'primereact/inputtext'; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import Link from 'next/link'; +import { CourseCreateType } from '@/types/courseCreateType'; +import { CourseType } from '@/types/courseType'; +import { Paginator } from 'primereact/paginator'; +import { NotFound } from '@/app/components/NotFound'; +import Redacting from '@/app/components/popUp/Redacting'; +import { getRedactor } from '@/utils/getRedactor'; +import { getConfirmOptions } from '@/utils/getConfirmOptions'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { myMainCourseType } from '@/types/myMainCourseType'; +import StreamList from '@/app/components/tables/StreamList'; +import { TabPanel, TabView } from 'primereact/tabview'; +import { TabViewChange } from '@/types/tabViewChange'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import useShortText from '@/hooks/useShortText'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { DataView } from 'primereact/dataview'; +import { FileWithPreview } from '@/types/fileuploadPreview'; +import { Dialog } from 'primereact/dialog'; +import { Dropdown, DropdownChangeEvent } from 'primereact/dropdown'; +import { AudenceType } from '@/types/courseTypes/AudenceTypes'; +import OpenStudentList from '@/app/components/tables/OpenStudentList'; +import { confirmDialog } from 'primereact/confirmdialog'; + +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useParams, useRouter } from 'next/navigation'; + +export default function Course() { + const { page } = useParams(); + + const { translations } = useLocalization(); + const { setMessage, setGlobalLoading, course, setMainCourseId, contextCourseDisplay, setContextCourseDisplay, contextStreamId, setContextStreamId, contextStreamIndex, setContextStreamIndex } = useContext(LayoutContext); + + const router = useRouter(); + const topRef = useRef(null); + const media = useMediaQuery('(max-width: 640px)'); + const tableMedia = useMediaQuery('(max-width: 577px)'); + + const [coursesValue, setValueCourses] = useState([]); + const [hasCourses, setHasCourses] = useState(false); + const [emptyCourses, setEmptyCourses] = useState(false); + const [courseValue, setCourseValue] = useState({ title: '', description: '', video_url: '', image: '' }); + const [editMode, setEditMode] = useState(false); + const [selectedCourse, setSelectedCourse] = useState(null); + const [formVisible, setFormVisible] = useState(false); + const [audenceTypeVisible, setAudenceTypeVisible] = useState(false); + const [forStart, setForStart] = useState(false); + const [skeleton, setSkeleton] = useState(false); + const [progressSpinner, setProgressSpinner] = useState(false); + const [pagination, setPagination] = useState<{ currentPage: number; total: number; perPage: number }>({ + currentPage: 1, + total: 0, + perPage: 0 + }); + const [activeIndex, setActiveIndex] = useState(contextStreamIndex); + const [imageState, setImageState] = useState(null); + const [editingLesson, setEditingLesson] = useState({ + title: '', + description: '', + video_url: '', + image: '', + created_at: '' + }); + + const [forStreamId, setForStreamId] = useState<{ id: number | null; title: string } | null>(contextCourseDisplay?.streamId); + const [sendStream, setSendStream] = useState<{ status: boolean; name: 'lock' | 'open' | 'wallet' | 'extra' | '' }>(contextCourseDisplay); + // const [globalCourseId, setGlobalCourseId] = useState<{ id: number | null; title: string | null } | null>(null); + // const [pageState, setPageState] = useState(Number(page)); + const [openTypes, setOpenTypes] = useState([]); + const [openCourseId, setOpenCourseId] = useState(null); + // const [copy_have, setCopy_have] = useState(false); + + const [isTall, setIsTall] = useState(false); + + const [filters, setFilters] = useState<{ + course_audience_type_id: number | null; + is_published: boolean | null; + status: boolean | null; + }>({ + course_audience_type_id: null, + is_published: null, + status: null + }); + + const audienceTypeOptions = [ + { label: translations.all, value: null }, + { label: translations.closed, value: 1 }, + { label: translations.openCourse, value: 2 }, + { label: translations.paid, value: 3 }, + { label: translations.notAuditItem, value: 4 } + ]; + + const publishedOptions = [ + { label: translations.all, value: null }, + { label: translations.published, value: true }, + { label: translations.notPublished, value: false } + ]; + + const statusOptions = [ + { label: translations.all, value: null }, + { label: translations.onReview, value: true }, + { label: translations.notOnReview, value: false } + ]; + + const handleFilterChange = (e: DropdownChangeEvent) => { + let value: any | null = null; + if (typeof e.value === 'object') { + value = null; + } else { + value = e.value; + } + + setFilters((prev) => ({ ...prev, [e.target.name]: value })); + }; + + const resetFilters = () => { + setFilters({ + course_audience_type_id: null, + is_published: null, + status: null + }); + // Here you would also refetch the data without filters + }; + + const showError = useErrorMessage(); + + const toggleSkeleton = () => { + setSkeleton(true); + setTimeout(() => { + setSkeleton(false); + }, 1000); + }; + + const handleEdit = async (e: { checked: boolean }, item: { status: boolean; id: number }) => { + setSkeleton(true); + const { id } = item; + const status = e.checked; + + const forSentStreams = { + course_id: id, + status: status ? 1 : 0 + }; + + const data = await veryfyCourse(forSentStreams); + if (data.success) { + handleFetchCourse(Number(page), filters); + // setPageState(1); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.updateSuccess, detail: '' } + }); + } else { + setSkeleton(false); + if (data.response.data.cause) { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data.response.data.cause } + }); + } else if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else if (data?.response?.status == '422') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.addCourseError, detail: '' } + }); + } + } + }; + + const fileUploadRef = useRef(null); + const clearFile = () => { + fileUploadRef.current?.clear(); + setImageState(null); + if (editMode) { + setEditingLesson( + (prev) => + prev && { + ...prev, + image: null + } + ); + // query + } else { + setCourseValue((prev) => ({ + ...prev, + image: null + })); + } + }; + + const handleFetchCourse = async ( + page: number, + filters: { + course_audience_type_id: number | null; + is_published: boolean | null; + status: boolean | null; + } + ) => { + setSkeleton(true); + const data = await fetchCourses(page, 10, filters?.course_audience_type_id, filters?.is_published, filters?.status); + + if (data && data?.courses) { + if (data?.courses?.data?.length > 0) { + setEmptyCourses(false); + } else { + setEmptyCourses(true); + } + setHasCourses(false); + setValueCourses(data.courses.data); + setPagination({ + currentPage: data?.courses?.current_page, + total: data?.courses?.total, + perPage: data?.courses?.per_page + }); + } else { + setHasCourses(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + setSkeleton(false); + }; + + const handleAddCourse = async () => { + setSkeleton(true); + const data = await addCourse(courseValue); + if (data?.success) { + handleFetchCourse(Number(page), filters); + // setPageState(1); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.successAdd, detail: '' } + }); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.addError, detail: '' } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + setSkeleton(false); + }; + + const handleDeleteCourse = async (id: number) => { + setSkeleton(true); + const data = await deleteCourse(id); + if (data?.success) { + // setGlobalCourseId(null); + handleFetchCourse(Number(page), filters); + // setPageState(1); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.deleteSuccess, detail: '' } + }); // messege - Успех! + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.deleteError, detail: '' } + }); // messege - Ошибка при добавлении + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setSkeleton(false); + }; + + const clearValues = () => { + setImageState(null); + setCourseValue({ title: '', description: '', video_url: '', image: '' }); + setEditingLesson({ title: '', description: '', video_url: '', image: '', created_at: '' }); + setEditMode(false); + setSelectedCourse(null); + }; + + const handleUpdateCourse = async () => { + setSkeleton(true); + const data = await updateCourse(selectedCourse, editingLesson); + if (data?.success) { + toggleSkeleton(); + handleFetchCourse(Number(page), filters); + // setPageState(1); + clearValues(); + setEditMode(false); + setSelectedCourse(null); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.updateSuccess, detail: '' } + }); // messege - Успех! + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.updateError, detail: '' } + }); // messege - Ошибка при изменении курса + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setSkeleton(false); + }; + + const onSelect = (e: FileUploadSelectEvent & { files: FileWithPreview[] }) => { + if (e.files?.length > 0) { + editMode + ? setEditingLesson((prev) => ({ + ...prev, + image: e.files[0] + })) + : setCourseValue((prev) => ({ + ...prev, + image: e.files[0] + })); + setImageState(e.files[0].objectURL); + } + }; + + const imageBodyTemplate = (product: CourseType) => { + const image = product.image; + + if (typeof image === 'string') { + return ( +
+ Course image +
+ ); + } + + return ( +
+ Course image +
+ ); + }; + + // Ручное управление пагинацией + const handlePageChange = (page: number) => { + // setGlobalCourseId(null); + // handleFetchCourse(page, filters); + // setPageState(page); + router.push(`/course/${page}`); + }; + + const edit = (rowData: number | null) => { + setEditMode(true); + setSelectedCourse(rowData); + setFormVisible(true); + }; + + const handleTabChange = (e: TabViewChange) => { + if (e.index === 0) { + // handleFetchLesson(); + } + // setActiveIndex(e.index); + setContextStreamIndex(e.index); + }; + + const handleFetchCourseOpenStatus = async () => { + setAudenceTypeVisible(true); + const data = await fetchCourseOpenStatus(); + if (data && Array.isArray(data)) { + setOpenTypes(data); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTryAgainLater, detail: '' } + }); // messege - Ошибка при изменении курса + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + const handleAddOpenTypes = async (course_audience_type_id: number, course_id: number) => { + setSkeleton(true); + const data = await addOpenTypes(course_audience_type_id, course_id); + if (data && data.success) { + handleFetchCourse(Number(page), filters); + setMessage({ + state: true, + value: { severity: 'success', summary: data.message, detail: '' } + }); + } else { + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setAudenceTypeVisible(false); + setSelectedCourse(null); + setSkeleton(false); + }; + + const onInbox = async (id: number, copy_have: boolean) => { + const data = await archiveCourse(id, copy_have); + if (data?.success) { + handleFetchCourse(Number(page), filters); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.archiveSuccess, detail: '' } + }); + } else { + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + const inboxConfirm = (id: number) => { + let copy_have = false; + confirmDialog({ + message: ( +
+ {translations.archiveCourseConfirmation} + {translations.archiveCourseNote} +
+ {translations.leaveCopy} + +
+
+ ), + header: translations.archiveCourse, + icon: 'pi pi-exclamation-triangle', + defaultFocus: 'accept', + // acceptLabel: translations.archive, + acceptLabel: translations.archive, + rejectLabel: translations.back, + rejectClassName: 'p-button-secondary reject-button', + className: 'w-[50%]', + accept: () => onInbox(id, copy_have) + }); + }; + + const itemTemplate = (shablonData: any) => { + return ( +
+
+ {/* Header: Title & Actions */} +
+
+ { + setMainCourseId(shablonData.id); + setGlobalLoading(true); + setTimeout(() => { + setGlobalLoading(false); + }, 900); + }} + > + {shablonData.title} + +
+ {tableMedia && ( +
+ +
+ )} +
+ + {/* Image & Main Info */} +
+
+ {typeof shablonData.image === 'string' ? ( + Course + ) : ( + No image + )} +
+ +
+
+ {translations.status}: + +
+ +
+ {translations.score}: + {shablonData?.max_score} +
+ +
+ {translations.publication}: + {shablonData.is_published ? : } +
+
+
+ + {/* Controls Section */} +
+
+ {translations.onReview} + +
+ + +
+ + {!tableMedia && ( +
+ +
+ )} +
+
+ ); + }; + + const courseFiltered = () => ( +
+
+ {/* Селект: Тип аудитории */} +
+ + +
+ + {/* Селект: На проверке */} +
+ + +
+ + {/* Селект: Опубликовано */} +
+ + +
+ + {/* Кнопка сброса */} +
+
+
+
+ ); + + const imagestateStyle = imageState || editingLesson.image ? 'flex gap-1 items-center justify-between flex-col sm:flex-row' : ''; + const imageTitle = useShortText(typeof editingLesson.image === 'string' ? editingLesson.image : '', 20); + + // usecallback + const callbackFetchCourse = useCallback(() => { + handleFetchCourse(Number(page), filters); + }, [page]); + + const callbackSetIndex = useCallback(() => { + // setActiveIndex(0); + setContextStreamIndex(0); + }, [activeIndex]); + + const callbackClose = useCallback(() => { + // setSendStream({ status: true, name: '' }); + setContextCourseDisplay({ status: true, name: '' }); + }, [activeIndex]); + + // useMemo + const memoForStreamId = useMemo(() => (forStreamId?.id ? forStreamId : null), [forStreamId?.id]); + + useEffect(() => { + // handleFetchCourseNumber((page), filters); + // setPageState(1); + setGlobalLoading(true); + setTimeout(() => { + setGlobalLoading(false); + }, 900); + }, []); + + useEffect(() => { + handleFetchCourse(Number(page), filters); + if (course?.data?.length > 5) { + setIsTall(true); + } else { + setIsTall(false); + } + }, [filters]); + + useEffect(() => { + const title = editMode ? editingLesson.title.trim() : courseValue.title.trim(); + if (title?.length > 0) { + setForStart(false); + } else { + setForStart(true); + } + }, [courseValue.title, editingLesson.title]); + + useEffect(() => { + const handleShow = async () => { + setProgressSpinner(true); + const data = await fetchCourseInfo(selectedCourse); + + if (data?.success) { + setProgressSpinner(false); + setEditingLesson({ + title: data.course.title || '', + video_url: data.course.video_url || '', + description: data.course.description || '', + image: data.course.image + }); + } else { + setProgressSpinner(false); + } + }; + + if (editMode) { + handleShow(); + } + }, [editMode]); + + useEffect(()=> { + setSendStream(contextCourseDisplay); + },[contextCourseDisplay]); + + useEffect(()=> { + setForStreamId(contextStreamId); + },[contextStreamId]); + + useEffect(() => { + setActiveIndex(contextStreamIndex); + }, [contextStreamIndex]); + + const tableData = useMemo(() => { + return coursesValue?.map((item) => ({ ...item, __isActive: forStreamId?.id === item.id })); + }, [coursesValue, forStreamId]); + + return ( +
+
+ {/* Мобильный курс */} + {media ? ( +
+ handleTabChange(e)} + activeIndex={activeIndex} + // className="main-bg" + pt={{ + nav: { className: 'flex flex-wrap justify-around' }, + panelContainer: { className: 'flex-1 pl-4' } + }} + > + {/* COURSE MOBILE */} + + {/* mobile table section */} + {hasCourses ? ( + <> +
+
+ + + ) : ( + <> +
+
+ + {skeleton ? ( +
+ {' '} + {' '} +
+ ) : ( + courseFiltered() + )} + + {/* {skeleton ?
: courseFiltered()} */} + + handlePageChange(e.page + 1)} + template={media ? 'FirstPageLink PrevPageLink NextPageLink LastPageLink' : 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'} + /> + + )} +
+ + {/* STREAMS MOBILE */} + +
+ {sendStream.name === 'lock' || sendStream.name === 'extra' ? ( + + ) : ( + + )} +
+
+
+
+ ) : ( + // Десктопный курс +
+ {sendStream.status ? ( +
+ {/* info section */} + {/* {skeleton ? ( + + ) : ( */} +
+

{translations.courses}

+
+ {/* // )} */} + + {skeleton ? ( +
+ {' '} +
+ ) : ( + courseFiltered() + )} + + {/* table section */} + {emptyCourses ? ( +

{translations.noData}

+ ) : hasCourses ? ( +

{translations.noCourses}

+ ) : ( + <> + {skeleton ? ( +
+ +
+ ) : ( +
+ {/* */} +
+ + rowIndex + 1} header="#" style={{ width: '20px' }}> + ( +
+ +
+ )} + body={imageBodyTemplate} + className="hover:bg-slate-50/50 transition-colors" + >
+ +
{translations.courseName}
} + body={(rowData) => ( + { + setGlobalLoading(true); + setTimeout(() => { + setGlobalLoading(false); + }, 1200); + setMainCourseId(rowData.id); + }} + key={rowData.id} + className="max-w-sm break-words" + > + {rowData.title} + + )} + className="hover:bg-slate-50/50 transition-colors" + >
+
{translations.courseStatus}
} + body={(rowData) => ( + + )} + className="hover:bg-slate-50/50 transition-colors" + >
+
{translations.score}
} + body={(rowData) => {rowData.max_score}} + >
+
{translations.onReview}
} + style={{ margin: '0 3px', textAlign: 'center' }} + body={(rowData) => ( + <> + + + )} + className="hover:bg-slate-50/50 transition-colors" + >
+
{translations.published}
} + style={{ margin: '0 3px', textAlign: 'center' }} + className="hover:bg-slate-50/50 transition-colors" + body={(rowData) => (rowData.is_published ? : )} + >
+
{translations.streams}
} + style={{ margin: '0 3px', textAlign: 'center' }} + body={(rowData) => { + const isChecked = forStreamId?.id === rowData.id; + return ( + <> + + + ); + }} + className="hover:bg-slate-50/50 transition-colors" + >
+ ( +
+ +
+ )} + /> +
+
+
+ handlePageChange(e.page + 1)} + template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink" + /> +
+
+ )} + + )} +
+ ) : ( +
+ {sendStream.name === 'lock' || sendStream.name === 'extra' ? ( + + ) : ( + + )} +
+ )} +
+ )} +
+ + {/* modal window */} + +
+
+
+ +
+
+
+ { + editMode + ? setEditingLesson((prev) => ({ + ...prev, + title: e.target.value + })) + : setCourseValue((prev) => ({ + ...prev, + title: e.target.value + })); + }} + /> +
+ {progressSpinner && } +
+
+ +
+ +
+ { + editMode + ? setEditingLesson((prev) => ({ + ...prev, + description: e.target.value + })) + : setCourseValue((prev) => ({ + ...prev, + description: e.target.value + })); + }} + /> + {progressSpinner && } +
+
+ +
+
+ {typeof imageState === 'string' ? ( + {translations.photo} + ) : editingLesson.image ? ( + {translations.photo} + ) : ( + '' + )} +
+
+ + + {courseValue.image || editingLesson.image ? ( +
+ {typeof editingLesson.image === 'string' && ( + <> + {imageTitle} + + )} +
+ ) : ( + jpeg, png, jpg + )} +
{(editingLesson.image || imageState) &&
+
+
+
+
+ + {/* open status window */} + { + if (!audenceTypeVisible) return; + setAudenceTypeVisible(false); + }} + > +
+ {skeleton ? ( + + ) : ( +
+ {openTypes?.map((item) => { + return ( +
{ + console.log(item, selectedCourse); + if (selectedCourse) { + handleAddOpenTypes(item?.id, selectedCourse); + } + }} + > +
+ + {item.title} +
+ {item?.description} +
+ ); + })} +
+ )} +
+
+
+ ); +} diff --git a/app/(main)/course/courseDetail/[course_id]/[lesson_id]/page.tsx b/app/(main)/course/courseDetail/[course_id]/[lesson_id]/page.tsx new file mode 100644 index 00000000..959a9ba1 --- /dev/null +++ b/app/(main)/course/courseDetail/[course_id]/[lesson_id]/page.tsx @@ -0,0 +1,7 @@ +import LessonView from '@/app/features/LessonView'; + +export default function LessonTheme() { + return ( + + ); +} diff --git a/app/(main)/course/courseDetail/[course_id]/default/page.tsx b/app/(main)/course/courseDetail/[course_id]/default/page.tsx new file mode 100644 index 00000000..ec2cc19b --- /dev/null +++ b/app/(main)/course/courseDetail/[course_id]/default/page.tsx @@ -0,0 +1,35 @@ +export default function CourseDefault({ searchParams }: { searchParams: { lang?: string } }) { + const lang = searchParams.lang || 'ru'; + + const ruText =

+ Выберите существующую тему из списка слева,
+ либо создайте новую для начала обучения. +

+ + const ruMoblieText = ( +

+ Выберите существующую тему,
+ либо создайте новую для начала обучения.
+ Нажмите на меню +

+ ); + + const kyText =

Окутууну баштоо үчүн сол жактагы тизмеден бар болгон теманы тандаңыз же жаңысын түзүңүз.

; + + const kyMobileText =

Окутууну баштоо үчүн бар болгон теманы тандаңыз же жаңысын түзүңүз.

; + + return ( +
+
+
+
+
+ + {lang === 'ru' ? ruText : lang === 'ky' ? kyText : ''} + {lang === 'ru' ? ruMoblieText : lang === 'ky' ? kyMobileText : ''} +
+
+
+
+ ); +} diff --git a/app/(main)/course/courseDetail/[course_id]/process/page.tsx b/app/(main)/course/courseDetail/[course_id]/process/page.tsx new file mode 100644 index 00000000..035d4179 --- /dev/null +++ b/app/(main)/course/courseDetail/[course_id]/process/page.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useParams, useRouter } from 'next/navigation'; +import { fetchThemes } from '@/services/courses'; +import { useEffect, useState } from 'react'; +import LessonView from '@/app/features/LessonView'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function CourseProcess() { + const { course_id } = useParams(); + const router = useRouter(); + const { language } = useLocalization(); + + const [lessonId, setLessonId] = useState(null); + + const handleFetchLesson = async () => { + const data = await fetchThemes(Number(course_id) || null, null) + + if(data && data?.lessons?.data?.length > 0){ + const firstLesson = data?.lessons?.data[0]; + if(firstLesson?.id){ + setLessonId(firstLesson.id); + } else { + router.replace(`/course/courseDetail/${course_id}/default?lang=${language}`); + } + } else { + router.replace(`/course/courseDetail/${course_id}/default?lang=${language}`); + } + } + + useEffect(()=> { + handleFetchLesson(); + },[]); + + return ( +
+ {lessonId && } +
+ ); +} diff --git a/app/(main)/course/detail/[course_Id]/[lesson_id]/page.tsx b/app/(main)/course/detail/[course_Id]/[lesson_id]/page.tsx new file mode 100644 index 00000000..6825d7be --- /dev/null +++ b/app/(main)/course/detail/[course_Id]/[lesson_id]/page.tsx @@ -0,0 +1,786 @@ +'use client'; + +import LessonDocument from '@/app/components/lessons/LessonDocument'; +import LessonForum from '@/app/components/lessons/LessonForum'; +import LessonLink from '@/app/components/lessons/LessonLink'; +import LessonPractica from '@/app/components/lessons/LessonPractica'; +import LessonTest from '@/app/components/lessons/LessonTest'; +import LessonVideo from '@/app/components/lessons/LessonVideo'; +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { fetchLessonShow } from '@/services/courses'; +import { addLesson, deleteStep, fetchElement, fetchSteps, fetchTypes, stepSequenceUpdate } from '@/services/steps'; +import { mainStepsType } from '@/types/mainStepType'; +import { getConfirmOptions } from '@/utils/getConfirmOptions'; +import { useParams, useRouter } from 'next/navigation'; +import { Button } from 'primereact/button'; +import { confirmDialog } from 'primereact/confirmdialog'; +import { Dialog } from 'primereact/dialog'; +import { InputText } from 'primereact/inputtext'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function LessonStep() { + const { translations } = useLocalization(); + const param = useParams(); + const course_id = param.course_Id; + const page = param.page; + const scrollRef = useRef(null); + const router = useRouter(); + const prevLessonsRef = useRef | null>( null); + const prevStepsRef = useRef([]); + + const media = useMediaQuery('(max-width: 640px)'); + const { setMessage, contextFetchThemes, contextThemes, deleteQuery } = useContext(LayoutContext); + const showError = useErrorMessage(); + + const [lessonInfoState, setLessonInfoState] = useState<{ title: string; documents_count: string; usefullinks_count: string; videos_count: string; from: string; to: string } | null>(null); + const [formVisible, setFormVisible] = useState(false); + const [types, setTypes] = useState<{ id: number; title: string; name: string; logo: string }[]>([]); + const [steps, setSteps] = useState([]); + const [element, setElement] = useState<{ content: any | null; step: mainStepsType } | null>(null); + const [selectedId, setSelectId] = useState(null); + const [hasSteps, setHasSteps] = useState(false); + const [themeNull, setThemeNull] = useState(false); + const [lesson_id, setLesson_id] = useState(null); + const [sequence_number, setSequence_number] = useState(null); + const [skeleton, setSkeleton] = useState(false); + const [stepSkeleton, setStepSkeleton] = useState(false); + const [wasCreated, setWasCreated] = useState(false); + const [lastStep, setLastStep] = useState(null); + const [draggedId, setDraggedId] = useState(''); + const [toggleDragSteps, setToggleDragSteps] = useState(false); + const [toggleDocGenerate, setToggleDocGenerate] = useState(false); + const [documentSteps, setDocumentSteps] = useState([]); + + const changeUrl = (lessonId: number | null) => { + router.replace(`/course/detail/${course_id}/${lessonId ? lessonId : null}`); + }; + + const handleShow = async (LessonId: number | null) => { + setSkeleton(true); + const data = await fetchLessonShow(LessonId); + if (data?.lesson) { + setSkeleton(false); + setLessonInfoState({ title: data.lesson.title, videos_count: data.lesson.videos_count, usefullinks_count: data.lesson.usefullinks_count, documents_count: data.lesson.documents_count, from: data.lesson.from, to: data.lesson.to }); + } else { + setSkeleton(false); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + // skeleton = false + } + }; + + const handleFetchTypes = async () => { + setFormVisible(true); + setSkeleton(true); + const data = await fetchTypes(); + if (data && Array.isArray(data)) { + setTypes(data); + setSkeleton(false); + } else { + setSkeleton(false); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const handleFetchSteps = async (lesson_id: number | null) => { + setSkeleton(true); + const data = await fetchSteps(Number(lesson_id)); + + if (data.success) { + if (data.steps.length < 1) { + setHasSteps(true); + } else { + setHasSteps(false); + setSteps(data.steps); + } + } else { + setHasSteps(false); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + setSkeleton(false); + }; + + const handleAddLesson = async (lessonId: number, typeId: number) => { + setFormVisible(false); + const forSequence_number = lastStep && lastStep > 0 ? (!sequence_number || sequence_number < 1 ? lastStep + 1 : sequence_number) : sequence_number; + + const data = await addLesson({ lesson_id: lessonId, type_id: typeId }, forSequence_number || 0); + if (data.success) { + setSequence_number(null); + setWasCreated(true); + handleFetchSteps(lessonId); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.successAdd, detail: '' } + }); + } else { + if (data?.message) { + const teachers = () => { + if (data?.response?.data.teachers?.length) { + return ( +
+ {data.response?.data.teachers?.map((item: any, idx: number) => { + return ( +
+ + {item?.last_name} {item?.name && item?.name[0] + '.'} {item?.father_name && item?.father_name.length > 1 && item?.father_name[0] + '.'} + + {item?.streams?.map((item: number) => item + ' ')} +
+ ); + })} +
+ ); + } else { + return ''; + } + }; + + setMessage({ + state: true, + value: { severity: 'error', summary: data?.response?.data?.message, detail:
{teachers()}
} + }); + } else if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.addError, detail: '' } + }); + } + } + }; + + const handleFetchElement = async (stepId: number) => { + if (lesson_id) { + setStepSkeleton(true); + const data = await fetchElement(Number(lesson_id), stepId); + + if (data.success) { + setElement({ content: data.content, step: data.step }); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + setStepSkeleton(false); + } + }; + + const handleDeleteStep = async () => { + const data = await deleteStep(Number(lesson_id), Number(selectedId)); + if (data.success) { + contextFetchThemes(Number(course_id), null); + handleFetchSteps(Number(lesson_id)); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.deleteSuccess, detail: '' } + }); + setSelectId(null); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.deleteError, detail: '' } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + const handleDrop = (e: any, index: number) => { + const startI = e.dataTransfer.getData('index'); + + if (startI !== index) { + const newStepsPosition = []; + if (steps && steps?.length > 0) { + const arr = [...steps]; + [arr[startI], arr[index]] = [arr[index], arr[startI]]; + if (arr) { + for (let i = 0; i < arr?.length; i++) { + const step = { id: arr[i]?.id, step: i + 1 }; + newStepsPosition?.push(step); + } + } + } + if (newStepsPosition) { + handleUpdateSequence(newStepsPosition); + } + } + }; + + const handleUpdateSequence = async (steps: { id: number; step: number }[]) => { + setSkeleton(true); + const secuence = await stepSequenceUpdate(lesson_id ? Number(lesson_id) : null, steps); + + if (secuence?.success) { + handleFetchSteps(lesson_id); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.updateSuccess, detail: '' } + }); + } else { + if (secuence?.response?.status) { + if (secuence?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: secuence?.response?.data?.message } + }); + } else { + showError(secuence.response.status); + } + } + } + setSkeleton(false); + }; + + // РАБОЧИЙ ВАРИАНТ + // useEffect(() => { + // if (Array.isArray(steps) && steps.length > 0) { + // const stepId = wasCreated ? steps[steps.length - 1].id : steps[0].id; + // setSelectId(stepId); + // handleFetchElement(stepId); + // setWasCreated(false); + // } + // }, [steps]); + // РАБОЧИЙ ВАРИАНТ + + useEffect(() => { + if (Array.isArray(steps) && steps.length > 0) { + let max = 0; + steps?.forEach((item) => { + if (item.step > max) { + max = item.step; + } + }); + + if (max) { + setLastStep(max); + } + + let stepId: number | null = null; + + if (wasCreated) { + // находим шаг, которого не было в предыдущем массиве + const prevIds = prevStepsRef.current.map((s) => s.id); + const newStep = steps.find((s) => !prevIds.includes(s.id)); + + if (newStep) { + stepId = newStep.id; + } else { + stepId = steps[steps.length - 1].id; // fallback: берем последний + } + } else { + if (selectedId) { + stepId = selectedId; + } else { + stepId = steps[0].id; // если просто загрузка — берем первый + } + } + + if (stepId !== null) { + setSelectId(stepId); + handleFetchElement(stepId); + } + + // сохраняем текущие steps для следующего сравнения + prevStepsRef.current = steps; + setWasCreated(false); + } + }, [steps]); + + useEffect(() => { + if (lesson_id) { + handleShow(lesson_id); + changeUrl(lesson_id); + } + }, [lesson_id]); + + useEffect(() => { + contextFetchThemes(Number(course_id), null); + }, [course_id]); + + // заменяем первый useEffect + useEffect(() => { + const lessons = contextThemes?.lessons?.data ?? []; + // console.log(lessons); + + // делаем "снимок" важных полей (id + title) + const snapshot = lessons.map((l: any) => ({ id: l.id, title: l.title ?? '' })); + const prev = prevLessonsRef.current; + + const isSameSnapshot = prev && prev.length === snapshot.length && prev.every((p, i) => p.id === snapshot[i].id && p.title === snapshot[i].title); + + // обновляем ref для следующего раза + prevLessonsRef.current = snapshot; + + // если ничего по-сути не поменялось — ничего не делаем + if (isSameSnapshot) return; + + // дальше — твоя логика, но чуть упрощённая и аккуратная + if (!lessons || lessons?.length < 1) { + setLesson_id(null); + setThemeNull(true); + return; + } else { + setThemeNull(false); + } + + let chosenId: number | null = null; + if (param.lesson_id && param.lesson_id !== 'null') { + const urlId = Number(param.lesson_id); + const exists = lessons.some((l: { id: number }) => l.id === urlId); + chosenId = exists ? urlId : lessons[0].id; + } else { + chosenId = lessons[0]?.id; + } + + // если выбор изменился — setLesson_id вызовет второй useEffect и всё остальное произойдёт там + if (lesson_id !== chosenId) { + setLesson_id(chosenId); + } else { + // если lesson_id не поменялся, но изменился контент выбранной темы (например, title), + // можно аккуратно обновить отображение (handleShow) — только если у нас реально поменялся title + const prevSelected = prev?.find((p) => p.id === lesson_id); + const currSelected = snapshot.find((p: { id: number }) => p.id === lesson_id); + if (lesson_id && prevSelected && currSelected && prevSelected.title !== currSelected.title) { + // вызываем только обновление показа — не трогаем fetchSteps, если id тот же + handleShow(lesson_id); + } + } + }, [contextThemes, deleteQuery, param.lesson_id /* оставил твои зависимости */]); + + useEffect(() => { + if (lesson_id && param.lesson_id !== String(lesson_id)) { + changeUrl(lesson_id); + } + if (lesson_id) { + handleFetchSteps(lesson_id); + handleShow(lesson_id); + } + }, [lesson_id]); + + useEffect(() => { + const element = scrollRef.current; + if (element) { + const handleWheelScroll = (event: any) => { + // 1. Проверяем, что это вертикальный скролл (колесо мыши) + if (event.deltaY !== 0) { + // 2. Отменяем стандартное вертикальное поведение прокрутки страницы + event.preventDefault(); + + // 3. Смещаем горизонтальную позицию (scrollLeft) + // на величину вертикального сдвига (event.deltaY) + element.scrollLeft += event.deltaY; + } + }; + + // Добавляем слушатель события 'wheel' + element.addEventListener('wheel', handleWheelScroll); + + // Функция очистки: удаляем слушатель при демонтировании компонента + return () => { + element.removeEventListener('wheel', handleWheelScroll); + }; + } + }, []); + + // ... остальная часть вашего компонента и JSX, где используется ref={scrollRef} + + const lessonInfo = ( +
+
+

+ {lessonInfoState?.title ? lessonInfoState?.title : '---'} +

+ {lessonInfoState?.from && lessonInfoState?.to && ( +
+
+ {translations.availableFrom} + {lessonInfoState?.from} + - + {lessonInfoState?.to} +
+
+ )} + {media && contextThemes && contextThemes?.max_sum_score ? ( +
+ {translations.courseScore} + {contextThemes?.max_sum_score} +
+ ) : ( + '' + )} +
+
+ ); + + const step = (item: mainStepsType, icon: string, step: number, idx: number) => { + return ( +
{ + e.dataTransfer.setData('index', String(idx)); + console.log('Перетаскиваем: ', idx); + }} + onDrop={(e) => handleDrop(e, idx)} + onDragOver={(e) => e.preventDefault()} + className="cursor-pointer flex flex-col items-center" + onClick={() => { + if (toggleDragSteps) { + setDraggedId(item?.id); + } else { + setSelectId(step); + handleFetchElement(step); + } + }} + > + + {idx + 1} + {item?.type?.name === 'practical' || item?.type?.name === 'test' ? ( + + ({item.score}) + + ) : ( + '' + )} + + +
+ +
+
+ ); + }; + + const handleDragStart = (id: number | string) => { + setDraggedId(id); + }; + + const handlePreparation = (steps: mainStepsType[]) => { + const forSteps = steps?.filter((item) => item?.type?.name === 'document' && item?.id_parent); + if (forSteps && forSteps?.length > 0) { + setToggleDragSteps(true); + setDocumentSteps(forSteps); + return true; + } else { + setToggleDragSteps(true); + setDocumentSteps([]); + return true; + } + }; + + if (themeNull) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* modal sectoin */} + { + if (!formVisible) return; + setFormVisible(false); + }} + > +
+
+ {translations.position} + { + setSequence_number(Number(e.target.value)); + }} + /> +
+ {skeleton ? ( + + ) : ( +
+ {types.map((item) => { + // if(item?.name === 'forum'){ + // return null; + // } + + return ( + +
+ + { + handleAddLesson(Number(lesson_id), item?.id); + }} + > + {item.title} + +
+
+ ); + })} +
+ )} +
+
+ + {/* info section */} + {lessonInfo} + + {/* steps section */} + {skeleton ? ( +
+ + + +
+ ) : ( +
+ {hasSteps ? ( +
+
+
+ ) : ( +
+ {toggleDocGenerate ? ( +
+
+ { + setToggleDocGenerate(false); + }} + > +
+ {translations.wordTestGeneration} +
+
+
+ ) : toggleDragSteps ? ( +
+
+
+ setToggleDragSteps(false)}> +
+ {translations.aiTestGeneration} + +
+
+
+ + {translations.testVariantsHint} {translations.optional} + + {/* Кол-о документов: {documentSteps?.length || 0} */} +
+
{' '} + {documentSteps?.length > 0 && ( +
= 6 ? 'right-shadow' : '') : steps.length >= 12 ? 'right-shadow' : ''}`}> + {documentSteps?.map((item, idx) => { + return ( +
+
handleDragStart(item.id)}> + {step(item, item.type.logo, item.id, idx)} +
+
+ ); + })} +
+ )} +
+ ) : ( +
= 6 ? 'right-shadow' : '') : steps.length >= 12 ? 'right-shadow' : ''}`}> + {steps.map((item, idx) => { + return ( +
+ {step(item, item.type.logo, item.id, idx)} +
+ ); + })} +
+ )} +
+ )} + +
+ {skeleton ? ( + + ) : ( + !toggleDragSteps && + !toggleDocGenerate && ( + + ) + )} +
+
+ )} + + {hasSteps && ( +
+ +
+ )} + {element?.step.type.title && ( +
+ {element?.step.type.title} - +
+ ({element?.step?.step === 0 ? '1' : element?.step?.step} {translations.positionUnit}) +
+
+ )} + + {stepSkeleton ? ( + + ) : ( + <> + {element?.step.type.name === 'document' && ( + { + handleFetchElement(stepId); + handleFetchSteps(lesson_id); + }} + clearProp={hasSteps} + /> + )} + {element?.step.type.name === 'video' && ( + { + handleFetchElement(stepId); + handleFetchSteps(lesson_id); + }} + clearProp={hasSteps} + /> + )} + + {element?.step.type.name === 'test' && ( + handlePreparation(steps)} + docPreparationTrue={() => { + setToggleDocGenerate(true); + }} + docPreparationFalse={() => { + setToggleDocGenerate(false); + }} + aiTestStat={toggleDragSteps} + docGenerageState={toggleDocGenerate} + aiTestSet={() => setToggleDragSteps(false)} + forAiTestId={draggedId} + aiTestSteps={documentSteps} + element={element?.step} + content={element?.content} + fetchPropElement={(stepId) => { + handleFetchElement(stepId); + handleFetchSteps(lesson_id); + }} + fetchPropThemes={() => contextFetchThemes(Number(course_id), null)} + clearProp={hasSteps} + /> + )} + {element?.step.type.name === 'practical' && ( + { + handleFetchElement(stepId); + handleFetchSteps(lesson_id); + }} + fetchPropThemes={() => contextFetchThemes(Number(course_id), null)} + clearProp={hasSteps} + /> + )} + {element?.step.type.name === 'link' && ( + { + handleFetchElement(stepId); + handleFetchSteps(lesson_id); + }} + clearProp={hasSteps} + /> + )} + {element?.step.type.name === 'forum' && ( + { + handleFetchElement(stepId); + handleFetchSteps(lesson_id); + }} + clearProp={hasSteps} + /> + )} + + )} + +
+
+
+ ); +} diff --git a/app/(main)/dashboard/page.tsx b/app/(main)/dashboard/page.tsx new file mode 100644 index 00000000..db017104 --- /dev/null +++ b/app/(main)/dashboard/page.tsx @@ -0,0 +1,325 @@ +'use client'; + +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { ContributionDay } from '@/types/ContributionDay'; +import ActivityPage from '@/app/components/Contribution'; +import Link from 'next/link'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { fetchDashboardPerformance, fetchTeacherDashboard } from '@/services/dashboard/workingDashboard'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { useRouter } from 'next/navigation'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import { Chart } from 'primereact/chart'; +import MyDateTime from '@/app/components/MyDateTime'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function Dashboard() { + interface CourseTotalLastMonth { + total: number; + last_month: number; + } + + interface CourseStatisticApi { + all: CourseTotalLastMonth; + lock: CourseTotalLastMonth; + myActiveDays: string[]; + open: CourseTotalLastMonth; + published: number; + wallet: CourseTotalLastMonth; + } + + interface InfoType { + formula: string; + sections: { name: string; weight: string; description: string }[]; + title: string; + } + + interface PerformanceType { + course_sync_score: string; + created_at: string; + details: { courses_count: number; notifs_count: number; courses: number; notifs: number }; + id: number; + id_edu_year: number; + notification_score: string; + study_from: string; + study_to: string; + total_rate: string; + updated_at: string; + user_id: number; + } + + type OptionsType = Intl.DateTimeFormatOptions; + + const { user, departament } = useContext(LayoutContext); + const { language, translations } = useLocalization(); + + const ref = useRef(null); + const media = useMediaQuery('(max-width: 640px)'); + const router = useRouter(); + + const [courses, setCourses] = useState(null); + const [hasCourses, setHasCourses] = useState(false); + const [contribution, setContribution] = useState(null); + const [skeleton, setSkeleton] = useState(false); + const [performance, setPerformance] = useState(null); + const [info, setInfo] = useState(null); + + const [chartData, setChartData] = useState({}); + const [chartOptions, setChartOptions] = useState({}); + + const [chartPieData, setChartPieData] = useState({}); + const [chartPieOptions, setChartPieOptions] = useState({}); + + const options: OptionsType = { + year: '2-digit', + month: 'short', // 'long', 'short', 'numeric' + day: '2-digit', + hour12: false // 24-часовой формат + }; + + const kpdTitle = 'Мугалимдердин эффективдүүлүгүн эсептөө методикасы'; + const formulf = '(Курстун баллы + Билдирүү баллы) / 2'; + const timelinessAssessment = 'Семестрдин башталышына салыштырмалуу курстук тапшырмалардын өз убагында аткарылышы бааланат.'; + const notificationAssessment = 'Билдирмелерге жооп берүү убактысы (түзүлгөндөн ачылганга чейин) бааланат.'; + + const hanldeTeacherDashboard = async () => { + setSkeleton(true); + const data = await fetchTeacherDashboard(); + + if (data && data?.all) { + if (data?.myActiveDays) { + setContribution(data.myActiveDays); + } + setHasCourses(false); + setCourses(data); + } else { + setHasCourses(true); + } + setSkeleton(false); + }; + + const hanldeFetchPerformance = async () => { + const data = await fetchDashboardPerformance(); + + if (data && data?.performance) { + // setHasCourses(false); + setPerformance(data?.performance); + setInfo(data?.info); + } else { + // setHasCourses(true); + } + }; + + useEffect(() => { + const documentStyle = getComputedStyle(document.documentElement); + const data = { + labels: [info?.sections[0]?.name, info?.sections[1]?.name], + datasets: [ + { + data: [50, 50], + backgroundColor: [documentStyle.getPropertyValue('--blue-500'), documentStyle.getPropertyValue('--yellow-500'), documentStyle.getPropertyValue('--green-500')], + hoverBackgroundColor: [documentStyle.getPropertyValue('--blue-400'), documentStyle.getPropertyValue('--yellow-400'), documentStyle.getPropertyValue('--green-400')] + } + ] + }; + const options = { + plugins: { + legend: { + position: 'bottom', + labels: { + usePointStyle: true + } + } + } + }; + + setChartPieData(data); + setChartPieOptions(options); + }, [info]); + + useEffect(() => { + const data = { + labels: [translations.coursesConnection + performance?.course_sync_score + '%', translations.notificationsConnection + performance?.notification_score + '%', translations.totalRating + performance?.total_rate + '%'], + datasets: [ + { + label: translations.statistics, + data: [performance?.course_sync_score, performance?.notification_score, performance?.total_rate], + backgroundColor: ['rgba(255, 159, 64, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(153, 102, 255, 0.2)'], + borderColor: ['rgb(255, 159, 64)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)'], + borderWidth: 1 + } + ] + }; + const options = { + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true + } + } + }; + + setChartData(data); + setChartOptions(options); + }, [performance, translations]); + + useEffect(() => { + hanldeTeacherDashboard(); + hanldeFetchPerformance(); + }, []); + + useEffect(() => { + if (media) { + if (ref.current) { + ref.current.scrollLeft = ref.current.scrollWidth; + } + } + }, [media]); + + if (!user?.is_student) { + return ( +
+ {/* user info */} +
+

+ {user ? ( +
+ {user?.last_name} {user?.name && user.name[0] + '.'} {user?.father_name && user.father_name[0] + '.'} + + {translations.teacher} {user?.is_working && departament?.name && `• ${translations.headOfDepartment}`} + +
+ ) : ( + '---' + )} +

+
+ + {/* statistic */} + {skeleton ? ( +
+ + + + +
+ ) : ( + !hasCourses && ( +
+ +
+ {translations.allCourses} + +
+
{courses?.all?.total}
+ {courses?.all.last_month && courses?.all.last_month > 0 ? ( + + {courses?.all.last_month} {translations.createdLast30Days} + + ) : ( + '' + )} + + +
+ {translations.closedCourses} + +
+
{courses?.lock?.total}
+ {courses?.lock.last_month && courses?.lock.last_month > 0 ? ( + + {courses?.lock.last_month} {translations.createdLast30Days} + + ) : ( + '' + )} + + +
+ {translations.openCoursesTitle} + +
+
{courses?.open?.total}
+ {courses?.open.last_month && courses?.open.last_month > 0 ? ( + + {courses?.open.last_month} {translations.createdLast30Days} + + ) : ( + '' + )} + + +
+ {translations.paidCourses} + +
+
{courses?.wallet?.total}
+ {courses?.wallet.last_month && courses?.wallet.last_month > 0 ? ( + + {courses?.wallet.last_month} {translations.createdLast30Days} + + ) : ( + '' + )} + +
+ ) + )} + + {/* activity */} +
+

+ {translations.activity} +

+ +
+ + {/* kpd */} +
+

{language === 'ky' ? kpdTitle : info?.title}

+ + {translations.reportFor} + {} -{} + +
+
+ + {translations.formula} {language === 'ky' ? formulf : info?.formula} + + {language === 'ky' ? timelinessAssessment : info?.sections[0]?.description} + {language === 'ky' ? notificationAssessment : info?.sections[1]?.description} +
+
+ + + + +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ ); + } else { + router.push('/'); + } +} diff --git a/app/(main)/documentation/index.module.css b/app/(main)/documentation/index.module.css deleted file mode 100644 index acaa1030..00000000 --- a/app/(main)/documentation/index.module.css +++ /dev/null @@ -1,16 +0,0 @@ -@media screen and (max-width: 991px) { - .video-container { - position: relative; - width: 100%; - height: 0; - padding-bottom: 56.25%; - } - - .video { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } -} \ No newline at end of file diff --git a/app/(main)/documentation/page.tsx b/app/(main)/documentation/page.tsx deleted file mode 100644 index b958c674..00000000 --- a/app/(main)/documentation/page.tsx +++ /dev/null @@ -1,241 +0,0 @@ -/* eslint-disable @next/next/no-sync-scripts */ -import React from 'react'; - -const Documentation = () => { - return ( - <> -
-
-
-

Current Version

-

Next v13, React v18, Typescript with PrimeReact v10

- -
Getting Started
-

- Sakai is an application template for React based on the popular{' '} - - Next.js - {' '} - framework with new{' '} - - App Router - - . To get started, clone the{' '} - - repository - {' '} - from GitHub and install the dependencies with npm or yarn. -

-
-                            {`"npm install" or "yarn"`}
-                        
- -

- Next step is running the application using the start script and navigate to http://localhost:3000/ to view the application. That is it, you may now start with the development of your application using the Sakai - template. -

- -
-                            {`"npm run dev" or "yarn dev"`}
-                        
- -
Dependencies
-

Dependencies of Sakai are listed below and needs to be defined at package.json.

- -
-                            {`"primereact": "^9.6.2",                    //required: PrimeReact components
-"primeicons": "^6.0.1",                    //required: Icons
-"primeflex": "^3.3.0",                     //required: Utility CSS classes
-`}
-                        
- -
Structure
-

Sakai consist of a couple of folders where demos and core layout have been separated.

-

- There are two{' '} - - route groups - {' '} - under the app folder; {`(main)`} represents the pages that reside in the main dashboard layout whereas {`(full-page)`}{' '} - groups the pages with full page content such as landing page or a login page. -

-
    -
  • - layout/: Main layout files -
  • -
  • - demo/: Contains demo related utilities and helpers -
  • -
  • - app/: Demo pages -
  • -
  • - public/demo: Assets used in demos -
  • -
  • - public/layout: Assets used in layout such as a logo -
  • -
  • - styles/demo: Styles used in demos only -
  • -
  • - styles/layout: SCSS files of the core layout -
  • -
-
Route Groups
-

- Root Layout is the main of the application and it is defined at app/layout.tsx file. It contains the style imports and layout context provider. -

-
-                            
-                                {`"use client"
-import { LayoutProvider } from "./layout/context/layoutcontext";
-import { PrimeReactProvider } from "primereact/api";
-import "primereact/resources/primereact.css";
-...
-import "../styles/layout/layout.scss";
-import "../styles/demo/Demos.scss";
-
-interface RootLayoutProps {
-  children: React.ReactNode;
-}
-
-export default function RootLayout({ children }: RootLayoutProps) {
-  return (
-    
-      
-        
-      
-      
-        
-            {children}
-        
-      
-    
-  );
-}
-
-`}
-                            
-                        
-

- The pages that are using the layout elements need to be defined under the app/{'(main)'}/ folder. Those pages use the{' '} - app/{'(main)'}/layout.tsx as the root layout. -

-
-                            
-                                {`import { Metadata } from 'next';
-import Layout from "../../layout/layout";
-
-interface MainLayoutProps {
-  children: React.ReactNode;
-}
-
-export const metadata: Metadata = {
-    title: "Sakai by PrimeReact | Free Admin Template for Next.js",
-    ...
-  };
-
-export default function MainLayout({ children }: MainLayoutProps) {
-  return {children};
-}
-`}
-                            
-                        
-

- Only the pages that are using config sidebar wihout layout elements need to be defined under the app/{'(full-page)'}/ folder. Those pages use the{' '} - app/{'(full-page)'}/layout.tsx as the root layout. -

-
-                            
-                                {`import { Metadata } from 'next';
-import AppConfig from "../../layout/AppConfig";
-import React from "react";
-
-interface FullPageLayoutProps {
-  children: React.ReactNode;
-}
-
-export const metadata: Metadata = {
-    title: "Sakai by PrimeReact | Free Admin Template for Next.js",
-    ...
-  };
-
-export default function FullPageLayout({ children }: FullPageLayoutProps) {
-  return (
-    
-      {children}
-      
-    
-  );
-}
-`}
-                            
-                        
-
Default Configuration
-

- Initial layout configuration can be defined at the layout/context/layoutcontext.js file, this step is optional and only necessary when customizing the defaults. -

- -
-                            
-                                {`"use client";
-import React, { useState } from 'react';
-import Head from 'next/head';
-export const LayoutContext = React.createContext();
-
-export const LayoutProvider = (props) => {
-    const [layoutConfig, setLayoutConfig] = useState({
-        ripple: false,                          //toggles ripple on and off
-        inputStyle: 'outlined',                 //default style for input elements
-        menuMode: 'static',                     //layout mode of the menu, valid values are "static" or "overlay"
-        colorScheme: 'light',                   //color scheme of the template, valid values are "light", "dim" and "dark"
-        theme: 'lara-light-indigo',             //default component theme for PrimeReact
-        scale: 14                               //size of the body font size to scale the whole application
-    });
-}`}
-                            
-                        
- -
Menu
-

- Main menu is defined at AppMenu.js file based on{' '} - - MenuModel API - - . -

- -
PrimeReact Theme
-

- Sakai theming is based on the PrimeReact theme being used. -

- -
SASS Variables
-

- In case you'd like to customize the main layout variables, open _variables.scss file under layout folder. Saving the changes will be reflected instantly at your browser. -

- -
layout/_variables.scss
-
-                            
-                                {`
-/* General */
-$scale:14px;                    /* initial font size */ 
-$borderRadius:12px;             /* border radius of layout element e.g. card, sidebar */ 
-$transitionDuration:.2s;        /* transition duration of layout elements e.g. sidebar */ 
-`}
-                            
-                        
-
-
-
- - ); -}; - -export default Documentation; diff --git a/app/(main)/faculty/[id_kafedra]/[myedu_id]/[course_id]/page.tsx b/app/(main)/faculty/[id_kafedra]/[myedu_id]/[course_id]/page.tsx new file mode 100644 index 00000000..43406811 --- /dev/null +++ b/app/(main)/faculty/[id_kafedra]/[myedu_id]/[course_id]/page.tsx @@ -0,0 +1,194 @@ +'use client'; + +import LessonInfoCard from '@/app/components/lessons/LessonInfoCard'; +import { NotFound } from '@/app/components/NotFound'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { depCourseInfo } from '@/services/faculty'; +import { fetchDepartamentSteps } from '@/services/steps'; +import { mainStepsType } from '@/types/mainStepType'; +import { useParams } from 'next/navigation'; +import { Accordion, AccordionTab } from 'primereact/accordion'; +import { Dialog } from 'primereact/dialog'; +import { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function LessonCheck() { + const { setMessage, contextFetchThemes, setContextThemes, contextThemes } = useContext(LayoutContext); + const showError = useErrorMessage(); + const {translations} = useLocalization(); + const { id_kafedra, course_id } = useParams(); + + const [themes, setThemes] = useState([]); + const [themeShow, setThemeShow] = useState(false); + const [hasSteps, setHasSteps] = useState(false); + const [steps, setSteps] = useState([]); + const [activeIndex, setActiveIndex] = useState(0); + const [videoCall, setVideoCall] = useState(false); + const [video_link, setVideoLink] = useState(''); + const [courseInfo, setCourseInfo] = useState({title: ''}); + + const handleCourseInfo = async () => { + if (course_id) { + const data = await depCourseInfo(Number(course_id), Number(id_kafedra)); + if (data.success) { + setCourseInfo({title: data.course.title}) + } + } + }; + + const handleFetchSteps = async (lesson_id: number) => { + const data = await fetchDepartamentSteps(Number(lesson_id), Number(id_kafedra)); + if (data.success) { + if (data.steps?.length < 1) { + setHasSteps(true); + } else { + setHasSteps(false); + setSteps(data.steps); + } + } else { + setHasSteps(false); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const handleVideoCall = (value: string | null) => { + if (!value) { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.videoProccessError, detail: '' } + }); + } + + const url = new URL(typeof value === 'string' ? value : ''); + let videoId = null; + + if (url.hostname === 'youtu.be') { + // короткая ссылка, видео ID — в пути + videoId = url.pathname.slice(1); // убираем первый слеш + } else if (url.hostname === 'www.youtube.com' || url.hostname === 'youtube.com') { + // стандартная ссылка, видео ID в параметре v + videoId = url.searchParams.get('v'); + } + + if (!videoId) { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.videoProccessError, detail: '' } + }); + return null; // не удалось получить ID + } + // return `https://www.youtube.com/embed/${videoId}`; + setVideoLink(`https://www.youtube.com/embed/${videoId}`); + setVideoCall(true); + }; + + // ПРОСИМ КУРС ДЛЯ НАЗВАНИЯ И ТЕМЫ + useEffect(() => { + contextFetchThemes(Number(course_id), id_kafedra ? Number(id_kafedra) : null); + handleCourseInfo(); + return () => { + setContextThemes([]); + }; + }, []); + + // САМИ ТЕМЫ, присваиваем в локальные темы + useEffect(() => { + if (contextThemes.lessons?.data && contextThemes.lessons.data?.length > 0) { + setThemes(contextThemes?.lessons.data || []); + setThemeShow(false); + } else { + setThemeShow(true); + } + }, [contextThemes]); + + // просто посмотреть пока + useEffect(() => { + if (themes?.length > 0 && [activeIndex as number]) { + const lessonId = themes[activeIndex as number]?.id; + if (lessonId) { + handleFetchSteps(lessonId); + } + } + }, [themes, activeIndex]); + + return ( +
+ { + if (!videoCall) return; + setVideoCall(false); + }} + > +
+ +
+
+ {themeShow ? ( + + ) : ( +
+

Название курса: {courseInfo.title}

+ setActiveIndex(e.index)}> + {themes.map((item) => { + const content = steps.filter((j) => { + return j.content != null; + }); + + return ( + +
+ {hasSteps ? ( +

Данных нет

+ ) : content?.length > 0 ? ( + content.map((i, idx) => { + if (i.content) { + return ( +
+ { + + } +
+ ); + } + }) + ) : ( +

Данных нет

+ )} +
+
+ ); + })} +
+
+ )} +
+ ); +} diff --git a/app/(main)/faculty/[id_kafedra]/[myedu_id]/page.tsx b/app/(main)/faculty/[id_kafedra]/[myedu_id]/page.tsx new file mode 100644 index 00000000..584b25dc --- /dev/null +++ b/app/(main)/faculty/[id_kafedra]/[myedu_id]/page.tsx @@ -0,0 +1,178 @@ +'use client'; + +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { publishCourse } from '@/services/courses'; +import { depCourse } from '@/services/faculty'; +import { CourseType } from '@/types/courseType'; +import Link from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { Column } from 'primereact/column'; +import { DataTable } from 'primereact/datatable'; +import { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function CoursesDep() { + interface kafedraInfoType { + birth_date: string | null; + courses: []; + created_at: string; + email: string; + email_verified_at: string | null; + father_name: string; + id: number; + is_student: boolean; + is_working: boolean; + last_name: string; + myedu_id: number; + name: string; + phone: string; + pin: number; + updated_at: string; + } + + const router = useRouter(); + const {translations} = useLocalization(); + const { id_kafedra, myedu_id } = useParams(); + + const [courses, setCourses] = useState([]); + const [contentShow, setContentShow] = useState(false); + const [contentNull, setContentNull] = useState(false); + const [teacher, setTeacher] = useState<{id: number; myedu_id: number;} | null>(null); + const [forDisabled, setForDisabled] = useState(false); + const [skeleton, setSkeleton] = useState(false); + + const { setMessage } = useContext(LayoutContext); + const showError = useErrorMessage(); + + const fetchDepartamentCourse = async () => { + const data = await depCourse(Number(myedu_id), Number(id_kafedra)); + console.log(data); + + if (data && data?.courses) { + if(data.courses?.length < 1){ + setContentNull(true); + } else { + setContentNull(false); + } + setTeacher(data); + setCourses(data.courses); + setContentShow(false); + } else { + setContentShow(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); // messege - Ошибка при добавлении + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const publish = async (id_kafedra: number, course_id: number, status: boolean) => { + setForDisabled(true); + const data = await publishCourse(id_kafedra, teacher ? teacher.id : null, course_id, status); + if (data) { + setForDisabled(false); + fetchDepartamentCourse(); + setMessage({ + state: true, + value: { severity: 'success', summary: 'Успешно добавлен!', detail: '' } + }); + } else { + setForDisabled(false); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); // messege - Ошибка при добавлении + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const imageBodyTemplate = (product: CourseType) => { + const image = product.image; + + if (typeof image === 'string') { + return ( +
+ Course image +
+ ); + } + + return ( +
+ Course image +
+ ); + }; + + useEffect(() => { + fetchDepartamentCourse(); + }, []); + + if(contentNull) return + + return ( +
+ {skeleton ?
+ : contentShow ? ( + + ) : ( + <> +

Курсы

+ + rowIndex + 1} header="#" style={{ width: '20px' }}> + + ( + + {rowData.title} + + )} + > + ( +
+ {!rowData.is_published ? ( + + ) : ( + + )} +
+ )} + >
+
+ + )} +
+ ); +} diff --git a/app/(main)/faculty/[id_kafedra]/page.tsx b/app/(main)/faculty/[id_kafedra]/page.tsx new file mode 100644 index 00000000..2cbcbae2 --- /dev/null +++ b/app/(main)/faculty/[id_kafedra]/page.tsx @@ -0,0 +1,160 @@ +'use client'; + +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { fetchDepartament } from '@/services/faculty'; +import Link from 'next/link'; +import { useParams } from 'next/navigation'; +import { Column } from 'primereact/column'; +import { DataTable } from 'primereact/datatable'; +import { DataView } from 'primereact/dataview'; +import { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function Kafedra() { + interface kafedraInfoType { + birth_date: string | null; + courses: []; + created_at: string; + email: string; + email_verified_at: string | null; + father_name: string; + id: number; + is_student: boolean; + is_working: boolean; + last_name: string; + myedu_id: number; + name: string; + phone: string; + pin: number; + updated_at: string; + } + + const { id_kafedra } = useParams(); + const {translations} = useLocalization(); + + const [courses, setCourses] = useState([]); + const [contentShow, setContentShow] = useState(false); + const [skeleton, setSkeleton] = useState(false); + const [contentNull, setContentNull] = useState(false); + + const media = useMediaQuery('(max-width: 640px)'); + const tableMedia = useMediaQuery('(max-width: 577px)'); + + const { setMessage, setGlobalLoading } = useContext(LayoutContext); + const showError = useErrorMessage(); + + const handleFetchKafedra = async () => { + setSkeleton(true); + const data = await fetchDepartament(Number(id_kafedra)); + if (data && Array.isArray(data)) { + setSkeleton(false); + if (data.length > 0) { + setCourses(data); + setContentShow(false); + setContentNull(false); + } else { + setContentNull(true); + } + } else { + setSkeleton(false); + setContentShow(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const itemTemplate = (rowData: any) => { + return ( +
+
+ {/* Номер (rowIndex) можно добавить через внешний счетчик или props, но для DataView это сложнее */} + + {/* Заголовок */} +
+
+ + {rowData.last_name} {rowData.name} {rowData.father_name} + +
+
+ +
+
Количество курсов:{rowData.courses}
+
Утверждённых:{rowData.courses_published}
+
+ + {/*
{imageBodyTemplate(rowData)}
*/} +
+
+ ); + }; + + useEffect(() => { + setGlobalLoading(true); + setTimeout(() => { + setGlobalLoading(false); + }, 900); + handleFetchKafedra(); + }, []); + + if (contentNull) return ; + + return ( +
+ {skeleton ? ( +
+ +
+ ) : contentShow ? ( + + ) : ( +
+ {media ? ( + <>

Преподаватели

+ + ) : ( + + rowIndex + 1} header="#"> + ( + + {rowData.last_name} {rowData.name} {rowData.father_name} + + )} + > + ( +
+
+ {rowData.courses} +
({rowData.courses_published} утверждённых)
+
+
+ )} + >
+
+ )} +
+ )} +
+ ); +} diff --git a/app/(main)/faculty/page.tsx b/app/(main)/faculty/page.tsx new file mode 100644 index 00000000..87e024b0 --- /dev/null +++ b/app/(main)/faculty/page.tsx @@ -0,0 +1,95 @@ +'use client'; + +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { fetchKafedra } from '@/services/faculty'; +import Link from 'next/link'; +import { Column } from 'primereact/column'; +import { DataTable } from 'primereact/datatable'; +import { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function Faculty() { + interface City { + id: number | null; + name_ru: string; + id_faculty?: number | null; + } + + const showError = useErrorMessage(); + const { setMessage, setGlobalLoading } = useContext(LayoutContext); + const { translations } = useLocalization(); + + const [kafedra, setKafedra] = useState([{ name_ru: '', id: null }]); + const [selectShow, setSelectShow] = useState(false); + const [facultyShow, setFacultyShow] = useState(false); + const [skeleton, setSkeleton] = useState(false); + const [contentNull, setContentNull] = useState(false); + + const handleFetchKafedra = async () => { + setSkeleton(true); + const data = await fetchKafedra(); + if (data && Array.isArray(data)) { + setSkeleton(false); + if (data.length > 0) { + setKafedra(data); + setFacultyShow(false); + setContentNull(false); + } else { + setContentNull(true); + } + } else { + setSkeleton(false); + setFacultyShow(true); + setMessage({ state: true, value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + useEffect(() => { + handleFetchKafedra(); + + setGlobalLoading(true); + setTimeout(() => { + setGlobalLoading(false); + }, 900); + }, []); + + if (contentNull) return ; + + return ( +
+ {skeleton ? ( + + ) : ( + !selectShow && ( +
+

Кафедры

+ {facultyShow ? ( + + ) : ( + + rowIndex + 1} header="#" style={{ width: '20px', display: 'flex', justifyContent: 'center' }} className="start"> + ( + + {rowData.name_ru} + + )} + > + + )} +
+ ) + )} +
+ ); +} diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 5cb39fdb..2b079143 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -1,27 +1,38 @@ -import { Metadata } from 'next'; import Layout from '../../layout/layout'; interface AppLayoutProps { children: React.ReactNode; } -export const metadata: Metadata = { - title: 'PrimeReact Sakai', - description: 'The ultimate collection of design-agnostic, flexible and accessible React UI Components.', - robots: { index: false, follow: false }, - viewport: { initialScale: 1, width: 'device-width' }, +export const metadata = { + metadataBase: new URL('https://mooc.oshsu.kg'), + + title: { + default: 'Mooc ОшГУ', + template: '%s | Mooc ОшГУ' + }, + description: 'Платформа онлайн обучения ОшГУ', + + robots: { + index: true, + follow: true + }, + openGraph: { type: 'website', - title: 'PrimeReact SAKAI-REACT', - url: 'https://sakai.primereact.org/', - description: 'The ultimate collection of design-agnostic, flexible and accessible React UI Components.', - images: ['https://www.primefaces.org/static/social/sakai-react.png'], - ttl: 604800 - }, - icons: { - icon: '/favicon.ico' + title: 'Mooc ОшГУ', + description: 'Платформа онлайн обучения ОшГУ', + url: 'https://mooc.oshsu.kg', + // images: [ + // { + // url: '/', + // width: 1200, + // height: 630, + // alt: 'Mooc ОшГУ' + // } + // ] } -}; +} export default function AppLayout({ children }: AppLayoutProps) { return {children}; diff --git a/app/(main)/module/[page]/page.tsx b/app/(main)/module/[page]/page.tsx new file mode 100644 index 00000000..fecb0434 --- /dev/null +++ b/app/(main)/module/[page]/page.tsx @@ -0,0 +1,821 @@ +'use client'; + +import { fetchFaculty } from '@/services/faculty'; +import { fetchSpeciality } from '@/services/student/studentSearch'; +import { Dropdown } from 'primereact/dropdown'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import React, { useContext, useEffect, useState } from 'react'; +import ModuleChartCard from '@/app/components/cards/ModuleChartCard'; +import { dateUpdate, fetchModuleShedule, fetchSemestr, sheduleDiactivate, sheduleSave } from '@/services/module/module'; +import { Button } from 'primereact/button'; +import { confirmDialog } from 'primereact/confirmdialog'; +import { Panel } from 'primereact/panel'; +import { Dialog } from 'primereact/dialog'; +import { Calendar } from 'primereact/calendar'; +import { Nullable } from 'primereact/ts-helpers'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; +import MainTitle from '@/app/components/titles/MainTitle'; +import MyDateTime from '@/app/components/MyDateTime'; + +interface CurrentSpecialityType { + name_ru: string; + code: number | null; + id: number | null; +} + +interface SpecialityOptType extends CurrentSpecialityType {} + +type OptionsType = Intl.DateTimeFormatOptions; + +export default function Module() { + const { setMessage } = useContext(LayoutContext); + const showError = useErrorMessage(); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + + const [timeMode, setTimeMode] = useState({ name_ru: '', code: null, id: null }); + const [timeModeOptions, setTimeModeOptions] = useState(null); + + const [speciality, setSpecialyty] = useState(null); + const [specialityOptions, setSpecialityOptions] = useState([]); + const [id_speciality, setId_speciality] = useState(null); + + const [currentFacultyId, setCurrentFacultyId] = useState(null); + const [currentSpecialityId, setCurrentSpecialityId] = useState(null); + const [diactivateSp, setDiactivateSp] = useState(null); + const [diactivateCt, setDiactivateCt] = useState(null); + + const [period, setPeriod] = useState(null); + const [periodOptions, setPeriodOptions] = useState([ + { name_ru: 'Летний', id: 1 }, + { name_ru: 'Зимний', id: 2 } + ]); + + const [semestr, setSemestr] = useState<{ name_ru: string; id: number | null } | null>(null); + const [semestrOptions, setSemestrOptions] = useState([]); + + const [progressSpinner, setProgressSpinner] = useState(false); + const [miniSpinner, setMiniSpinner] = useState(false); + + const [connectIds, setConnectIds] = useState(null); + const [connects, setConnects] = useState([]); + + const [openIndex, setOpenIndex] = useState(null); + const [visible, setVisible] = useState(false); + const [dateUpdateVisible, setDateUpdateVisible] = useState(false); + const [allSelectFl, setAllSelectFl] = useState([]); + + const [emptySpeciality, setEmptySpeciality] = useState(false); + const [startDisplay, setStartDisplay] = useState(true); + + const [from, setFrom] = useState>(null); + const [to, setTo] = useState>(null); + const [editingFrom, setEditingFrom] = useState>(null); + const [editingTo, setEditingTo] = useState>(null); + const [updateDateId, setUpdateDateId] = useState(null); + const [id_stream, setId_stream] = useState(null); + + const handleFetchFaculty = async () => { + const data = await fetchFaculty(); + if (data && data?.length > 0) { + setTimeModeOptions(data); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const handleStudentSpeciality = async (id_faculty: number) => { + setMiniSpinner(true); + setStartDisplay(false); + const data = await fetchSpeciality(id_faculty); + // console.log(data); + if (data && data?.length) { + const newOpts = data; + newOpts.unshift({ id: null, code: 1, name_ru: translations.allSpecialities }); + setSpecialityOptions(newOpts); + } + setMiniSpinner(false); + }; + + const handleFetchModuleShedule = async (specialityIds: any, periodId: number | null, semesterId: number | null) => { + setProgressSpinner(true); + const data = await fetchModuleShedule(Array.isArray(specialityIds) ? specialityIds : [specialityIds], periodId, semesterId); + + if (data && data?.length > 0) { + setConnects(data); + setEmptySpeciality(false); + startSpecialityCheck(data); + startSheduleCheck(data); + } else { + setEmptySpeciality(true); + } + setProgressSpinner(false); + }; + + const handleFetchSemestr = async () => { + setProgressSpinner(true); + const data = await fetchSemestr(); + if (data && data?.length) { + setSemestrOptions(data); + setSemestr({ id: data[0]?.id, name_ru: data[0]?.name_ru }); + } + setProgressSpinner(false); + }; + + const handleSave = async () => { + setProgressSpinner(true); + const data = await sheduleSave(from, to, period?.id ? period?.id : null, semestr?.id ? semestr?.id : null, allSelectFl, connectIds); + if (data && data?.success) { + // setAllSelectFl([]); + // setConnectIds([]); + setMessage({ + state: true, + value: { severity: 'success', summary: data?.message, detail: '' } + }); + handleFetchModuleShedule(currentSpecialityId, period?.id ? period?.id : null, semestr?.id ? semestr?.id : null); + // setSemestrOptions(data); + // setSemestr({ id: data[0]?.id, name_ru: data[0]?.name_ru }); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + setProgressSpinner(false); + } + }; + + const handleDiactivate = async (spId: number | null, ctId: number | null) => { + setProgressSpinner(true); + const data = await sheduleDiactivate(period?.id ? period?.id : null, semestr?.id ? semestr?.id : null, spId, ctId); + if (data && data?.success) { + setMessage({ + state: true, + value: { severity: 'success', summary: data?.message, detail: '' } + }); + handleFetchModuleShedule(currentSpecialityId, period?.id ? period?.id : null, semestr?.id ? semestr?.id : null); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + setProgressSpinner(false); + } + }; + + const handleEdit = (id: number, e: { checked: boolean }, specialityId: number, active: boolean) => { + if (e.checked) { + if (connectIds) { + !connectIds?.includes(id) && setConnectIds([...connectIds, id]); + } else { + setConnectIds([id]); + } + } else { + if (active) { + setDiactivateCt(id); + confirm1(null, id); + } else { + setAllSelectFl((prev) => prev && prev?.filter((id) => id !== specialityId)); + setConnectIds((prev) => prev && prev?.filter((item) => item !== id)); + } + } + }; + + // const specialityWithAll = [{ id: null, code: 1, name_ru: translations.allSpecialities }, ...specialityOptions]; + + const startSheduleCheck = (connects: number[]) => { + if (connects) { + const activeStreamIds = connects + .flatMap((c: any) => c?.streams) + .filter((s: any) => s.schedule?.active) + .map((s: any) => s?.id_stream); + + // console.log(activeStreamIds); // [1, 3, 4] + if (activeStreamIds) { + setConnectIds(activeStreamIds); + } + } + }; + + const startSpecialityCheck = (speciality: number[]) => { + if (speciality) { + const activeSpecialityIds = speciality.filter((s: any) => s?.checking).map((s: any) => s?.id); + + if (activeSpecialityIds) { + setAllSelectFl(activeSpecialityIds); + } + } + }; + + const onDateUpdate = (id_speciality: number | null, item: any) => { + setDateUpdateVisible(true); + setId_speciality(id_speciality); + if (item) { + setUpdateDateId(item?.id); + setId_stream(item?.id_stream); + setEditingFrom(item?.schedule?.from); + setEditingTo(item?.schedule?.to); + } + }; + + const handleDateUpdate = async (idStream: number | null) => { + setDateUpdateVisible(false); + const data = await dateUpdate(editingFrom, editingTo, period?.id ? period?.id : null, semestr?.id ? semestr?.id : null, idStream, id_speciality); + console.log(data); + if (data?.success) { + handleFetchModuleShedule(currentSpecialityId, period?.id ? period?.id : null, semestr?.id ? semestr?.id : null); + } + setId_speciality(null); + setId_stream(null); + }; + + const confirm1 = (spId: number | null, ctId: number | null) => { + confirmDialog({ + message: translations.confirmChange, + header: translations.confirmation, + icon: 'pi pi-exclamation-triangle', + defaultFocus: 'accept', + acceptLabel: translations.change, + rejectLabel: translations.back, + rejectClassName: 'p-button-secondary reject-button', + accept: () => handleDiactivate(spId, ctId) + }); + }; + + const normalizeDate = (date: any): any => { + if (!date) return null; + + const d = new Date(date); + d.setHours(12, 0, 0, 0); // фикс timezone + return d; + }; + + const firstDateSearch = (stream: any) => { + // Проверяем, что schedule существует и у него есть оба заполненных поля + if (stream?.schedule?.from && stream?.schedule?.to) { + return { + from: stream.schedule.from, + to: stream.schedule.to + }; + } + return null; + }; + + const options: OptionsType = { + year: '2-digit', + month: 'short', // 'long', 'short', 'numeric' + day: '2-digit', + hour12: false // 24-часовой формат + }; + + // filters block + const renderFilters = () => ( +
+ {translations.moduleSchedule} +
+
+ {translations.selectFaculty} +
+ setTimeMode(e.value)} + placeholder={translations.selectFaculty} + className="w-full text-sm" + itemTemplate={(option) => {getLocalized(option, 'name') || option.name_ru}} + valueTemplate={(option) => { + if (!option) return {translations.selectFaculty}; + return {getLocalized(option, 'name') || option.name_ru}; + }} + /> +
+
+ +
+ {translations.speciality} +
+ { + setSpecialyty(e.value); + specialityProcessing(e.value); + }} + placeholder={translations.selectSpeciality} + className={`${specialityOptions?.length < 1 ? 'pointer-events-none opacity-50' : ''} w-full text-sm `} + itemTemplate={(option) => {getLocalized(option, 'name') || option.name_ru}} + valueTemplate={(option) => { + if (!option) return {translations.selectSpeciality}; + return {getLocalized(option, 'name') || option.name_ru}; + }} + /> + {miniSpinner && ( +
+ +
+ )} +
+
+ +
+ {translations.period} +
+ setPeriod(e.value)} + placeholder={translations.selectPeriod} + className={`${specialityOptions?.length < 1 ? 'pointer-events-none opacity-50' : ''} w-full text-sm`} + itemTemplate={(option) => {option.name_ru}} + valueTemplate={(option) => { + if (!option) return {translations.selectPeriod}; + return {option.name_ru}; + }} + /> +
+
+ +
+ {translations.semester} +
+ setSemestr(e.value)} + placeholder={translations.selectSemester} + className={`${specialityOptions?.length < 1 || miniSpinner ? 'pointer-events-none opacity-50' : ''} w-full text-sm`} + itemTemplate={(option) => {getLocalized(option, 'name') || option.name_ru}} + valueTemplate={(option) => { + if (!option) return {translations.selectSemester}; + return {getLocalized(option, 'name') || option.name_ru}; + }} + /> +
+
+
+ +
+
+
+ ); + + const saveBtnDisabled = connects?.length === 0; + + // main block + const renderMainContent = () => { + if (startDisplay) { + return ( +
+ +
+ ); + } + + if (progressSpinner) { + return ( +
+ +
+ ); + } + + if (emptySpeciality) { + return ( +
+ {translations.noData} +
+ ); + } + + return ( +
+ {connects.map((course: any, idx: number) => { + const isOpen = openIndex === idx; + return ( + { + setOpenIndex(isOpen ? null : idx); + // setConnectIds([]); + }} + className="w-full" + header={ +
+
+
+
+
+ +
+ + {idx + 1}. {getLocalized(course, 'name') || course.name_ru} + +
+
+ + {allSelectFl.some((s) => s === course?.id) ? ( +
+ { + onDateUpdate(course?.id, null); + }} + className={'cursor-pointer pi pi-calendar-plus text-[var(--mainColor)]'} + > +
+ ) : ( + '' + )} + +
+
+ {(() => { + let foundResult: any | { from: string; to: string } = null; + + // Просто вызываем find, не сохраняя его в константу + course?.streams?.find((item: any) => { + const result = firstDateSearch(item); + if (result) { + foundResult = result; + return true; + } + return false; + }); + + if (foundResult) { + return ( +
+ + - + +
+ ); + } + + return null; + })()} +
+
+ } + > +
+ {course?.streams?.length > 0 ? ( + course.streams.map((item: any) => ( + handleEdit(id, checked, course?.id, item?.schedule?.active)} + allIds={connectIds} + date={{ from: item?.schedule?.from, to: item?.schedule?.to }} + dateUpdate={(id: number) => onDateUpdate(null, item)} + /> + )) + ) : ( +
+ {translations.noData} +
+ )} +
+
+ ); + })} +
+
+
+ ); + }; + + const instructionSection = ( +
+ +
+ {/*

{translations.reductorInstruction}

*/} +

{translations.dateChangeWarn}

+
+
+ ); + + // save dialog + const renderDialog = () => ( + { + if (!visible) return; + setVisible(false); + }} + footer={footerContent} + > +
+
+ {instructionSection} +
+
+ {translations.moduleStart} + { + const date: any = normalizeDate(e.value); + if (date) { + setFrom(date); + } else { + setFrom(e.value); + } + }} + /> +
+
+ {translations.moduleEnd} + { + const date: any = normalizeDate(e.value); + if (date) { + setTo(date); + } else { + setTo(e.value); + } + }} + /> +
+
+
+
+
+ ); + + // date update dialog + const renderDateUpdateDialog = () => ( + { + if (!dateUpdateVisible) return; + setDateUpdateVisible(false); + setUpdateDateId(null); + setEditingFrom(null); + setEditingTo(null); + setId_speciality(null); + }} + footer={footerDateUpdate} + > +
+
+
+
+
+ {translations.moduleStart} + {miniSpinner && ( +
+ +
+ )} +
+ { + const date: any = normalizeDate(e.value); + if (date) { + setEditingFrom(date); + } else { + setEditingFrom(e.value); + } + }} + /> +
+
+
+ {translations.moduleEnd} + {miniSpinner && ( +
+ +
+ )} +
+ { + const date: any = normalizeDate(e.value); + if (date) { + setEditingTo(date); + } else { + setEditingTo(e.value); + } + }} + /> +
+
+
+
+
+ ); + + const specialityProcessing = (specialityParam: CurrentSpecialityType) => { + if (specialityParam) { + if (specialityParam?.code === 1) { + const forAllSpecialityIds: any = specialityOptions?.map((item: SpecialityOptType) => item?.id)?.filter((item) => item); + setCurrentSpecialityId(forAllSpecialityIds); + } else { + if (specialityParam?.id) { + setCurrentSpecialityId(specialityParam?.id); + } + } + } + }; + + useEffect(() => { + if (timeMode?.id) setCurrentFacultyId(timeMode?.id); + }, [timeMode]); + + useEffect(() => { + if (currentFacultyId) handleStudentSpeciality(currentFacultyId); + }, [currentFacultyId]); + + useEffect(() => { + if (specialityOptions?.length > 0) specialityProcessing(specialityOptions[0]); + }, [specialityOptions]); + + // Update period options when language changes + useEffect(() => { + setPeriodOptions([ + { name_ru: translations.summer, id: 1 }, + { name_ru: translations.winter, id: 2 } + ]); + + // Update selected period if it exists + if (period) { + const updatedPeriod = period.id === 1 ? { ...period, name_ru: translations.summer } : { ...period, name_ru: translations.winter }; + setPeriod(updatedPeriod); + } + }, [translations]); + + useEffect(() => { + handleFetchFaculty(); + handleFetchSemestr(); + }, []); + + const footerContent = ( +
+
+ ); + + // date update + const footerDateUpdate = ( +
+
+ ); + + return ( +
+ {/* filter */} + {renderFilters()} + + {/* main content */} + {renderMainContent()} + + {/* dialog */} + {renderDialog()} + + {/* date update dialog */} + {renderDateUpdateDialog()} +
+ ); +} diff --git a/app/(main)/notifications/page.tsx b/app/(main)/notifications/page.tsx new file mode 100644 index 00000000..26646503 --- /dev/null +++ b/app/(main)/notifications/page.tsx @@ -0,0 +1,214 @@ +'use client'; + +import MyDateTime from '@/app/components/MyDateTime'; +import { NotFound } from '@/app/components/NotFound'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { statusView } from '@/services/notifications'; +import { mainNotification } from '@/types/mainNotification'; +import { OptionsType } from '@/types/OptionsType'; +import { getConfirmOptions } from '@/utils/getConfirmOptions'; +import Link from 'next/link'; +import { confirmDialog } from 'primereact/confirmdialog'; +import { InputText } from 'primereact/inputtext'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import React, { useContext, useEffect, useState } from 'react'; +import { BottomNav } from '@/app/components/menu/MobileMenu'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import SubTitle from '@/app/components/titles/SubTitle'; +import MainTitle from '@/app/components/titles/MainTitle'; + +export default function MainNotificatoin() { + const { user, setMessage, contextNotifications, setContextNotifications, handleNotifications } = useContext(LayoutContext); + const { translations } = useLocalization(); + const media = useMediaQuery('(max-width: 640px)'); + const [notification, setNotification] = useState([]); + + const [searchSpinner, setSearchSpinner] = useState(false); + const [empty, setEmpty] = useState(false); + const [search, setSearch] = useState(null); + const [pendingChanges, setPendingChanges] = useState([]); + + const options: OptionsType = { + year: '2-digit', + month: 'short', // 'long', 'short', 'numeric' + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false // 24-часовой формат + }; + + const showError = useErrorMessage(); + + const handleStatusView = async (notification_id: number | null) => { + if (notification_id) { + const data = await statusView(Number(notification_id)); + if (data?.success) { + setMessage({ + state: true, + value: { severity: 'success', summary: translations.deleteSuccess, detail: '' } + }); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + if (user?.is_working || user?.is_student) { + handleNotifications(); + } + // setContextNotificationId(null); + } + }; + + const handleDeleteVisible = (id: number) => { + const options = getConfirmOptions(Number(id), () => handleStatusView(id)); + confirmDialog(options); + }; + + const NotificationItem = ({ notificate }: { notificate: mainNotification }) => { + let path = ''; + if (user?.is_working) { + if (notificate?.type?.type === 'practical') { + path = `/students/${notificate?.meta?.course_id}/${notificate?.meta?.connect_id}/${notificate?.meta?.stream_id}/${notificate?.meta?.student_id}/${notificate?.from_user?.id}/${notificate?.meta?.lesson_id}/${notificate?.meta?.step_id}`; + } else if (notificate?.type?.type === 'view') { + path = `/notifications`; + } + } else if (user?.is_student) { + if (notificate?.type?.type === 'practical') { + path = `/teaching/lessonView/${notificate?.meta?.lesson_id}/${notificate?.meta?.id_curricula}/${notificate?.meta?.stream_id}/${notificate?.meta?.step_id}`; + } + } + return ( +
+
+
+ {/*
+ + {notificate?.title} + +
*/} + + {notificate?.from_user?.last_name} + {notificate?.from_user?.name} + {notificate?.from_user?.father_name} + + + {notificate?.from_user?.last_name} + {notificate?.from_user?.name[0]}. + {notificate?.from_user?.father_name && notificate?.from_user?.father_name[0] != ' ' ? notificate?.from_user?.father_name[0] + '.' : ''} + +
+ {/* */} + handleDeleteVisible(notificate?.id)}> +
+
+ + {/* student message */} +
+ {/* + */} +
+
+ {notificate?.title} +
+ {notificate?.meta?.title && ( + <> + {translations.reason}: +
    +
  • {notificate?.meta?.title}
  • +
+ +
    +
  • {notificate?.meta?.description}
  • +
+ + )} +
+
+ {notificate?.type?.title} +
+
+

+ +

+
+
+ ); + }; + + useEffect(() => { + if (contextNotifications) { + if (contextNotifications.length > 0) { + setNotification(contextNotifications); + setEmpty(false); + } else { + setEmpty(true); + } + } + }, [contextNotifications]); + + useEffect(() => { + if (user?.is_working || user?.is_student) { + handleNotifications(); + } + }, [user]); + + return ( +
+
+ {translations.notifications} + + {/*
+ setSearch(e.target.value)} /> +
{!searchSpinner && }
+
{searchSpinner && }
+
*/} + {/* +
+
+ Избранные + +
+
+ Архив + +
+
*/} + + {/* main */} +
+ {empty ? ( +
+ +
+ ) : ( + notification?.map((item) => { + return ( +
+ +
+ ); + }) + )} +
+
+ {/*{media && user?.is_student && }*/} +
+ ); +} diff --git a/app/(main)/openCourse/[page]/page.tsx b/app/(main)/openCourse/[page]/page.tsx new file mode 100644 index 00000000..38d2faf7 --- /dev/null +++ b/app/(main)/openCourse/[page]/page.tsx @@ -0,0 +1,509 @@ +'use client'; + +import OpenCourseCard from '@/app/components/cards/OpenCourseCard'; +import OpenCourseShowCard from '@/app/components/cards/OpenCourseShowCard'; +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { fetchOpenCourses, openCourseShow, openCourseSignup, signupList } from '@/services/openCourse'; +import { depCategoryFetch, depLangFetch } from '@/services/roles/roles'; +import { CourseCategoryOption } from '@/types/openCourse/CourseCategoryOption'; +import { MainLangType } from '@/types/openCourse/MainLangType'; +import { useParams, useRouter } from 'next/navigation'; +import { Button } from 'primereact/button'; +import { Dropdown, DropdownChangeEvent } from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { Paginator } from 'primereact/paginator'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { Sidebar } from 'primereact/sidebar'; +import React, { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import MainTitle from '@/app/components/titles/MainTitle'; + +// types +interface CategoryId { + title: string; + id: number | null; + description: string; + name_ru?: string; + name_kg?: string; +} + +interface SelectLangType extends Pick { + name_ru?: string; + name_kg?: string; +} + +export default function OpenCourse() { + const { page } = useParams(); + const router = useRouter(); + const { setMessage } = useContext(LayoutContext); + const showError = useErrorMessage(); + const media = useMediaQuery('(max-width: 640px)'); + const { translations, language } = useLocalization(); + + const params = new URLSearchParams(); + const [coursesValue, setValueCourses] = useState([]); + const [courseDetail, setCourseDetail] = useState(null); + const [free, setFree] = useState<'free' | 'paid' | null>(null); + const [search, setSearch] = useState(''); + const [skeleton, setSkeleton] = useState(false); + const [hasCourses, setHasCourses] = useState(false); + const [emptyCourse, setEmptyCourse] = useState(false); + const [pagination, setPagination] = useState<{ currentPage: number; total: number; perPage: number }>({ + currentPage: 1, + total: 0, + perPage: 0 + }); + const [searchController, setSearchController] = useState(false); + const [showVisisble, setShowVisible] = useState(false); + const [progressSpinner, setProgressSpinner] = useState(false); + const [mainProgressSpinner, setMainProgressSpinner] = useState(false); + const [signUpList, setSignupList] = useState([]); + const [signupDisabled, setSignupDisebled] = useState(false); + + const [langSelectedId, setLangSelectedId] = useState({ title: translations.all, id: null, description: '' }); + const [depSelectLang, setSelectLang] = useState([{ title: translations.selectCategoryPlaceholder, id: null, description: '' }]); + const [language_id, setLanguage_id] = useState(null); + + const [categorySelectedId, setCategorySelectedId] = useState({ title: translations.all, id: null, description: '' }); + const [depCategoryes, setCategoryes] = useState([{ title: translations.selectCategoryPlaceholder, id: null, description: '' }]); + + const getLocalizedName = (item: any) => { + if (!item) return ''; + if (language === 'ky' && item.name_kg) return item.name_kg; + if (language === 'ru' && item.name_ru) return item.name_ru; + return item.name || item.title || ''; + }; + + const handleFetchOpenCourse = async (page: number, audence_type_id: number | string, search: string, categoryId: number | null, lang_id: number | null) => { + setSkeleton(true); + setMainProgressSpinner(true); + const data = await fetchOpenCourses(page, audence_type_id, search, categoryId, lang_id); + + if (data && Array.isArray(data.data)) { + setHasCourses(false); + if (data?.data?.length < 1) { + setEmptyCourse(true); + } else { + setEmptyCourse(false); + } + setValueCourses(data.data); + setPagination({ + currentPage: data.current_page, + total: data?.total, + perPage: data?.per_page + }); + } else { + setHasCourses(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setSkeleton(false); + setMainProgressSpinner(false); + }; + + const handleDepCategoryFetch = async () => { + const res = await depCategoryFetch(); + if (res && res?.length) { + const forCategoryes = res.map((item: { title: string; id: number; description: string; name_ru?: string; name_kg?: string }) => { + return { name: item?.title, id: item?.id, description: item?.description, name_ru: item?.name_ru, name_kg: item?.name_kg }; + }); + if (forCategoryes) { + forCategoryes.unshift({ name: translations.all, id: null, description: translations.showAllCourses }); + setCategoryes(forCategoryes); + } + } + }; + + // lang + const handleDepLangFetch = async () => { + const res = await depLangFetch(); + if (res && res?.success) { + const selectLangList = res?.data; + if (selectLangList) { + selectLangList?.unshift({ title: translations.all, id: null, description: translations.showAllCourses }); + setSelectLang(res?.data); + } + } + }; + + const handleCourseShow = async (course_id: number) => { + setShowVisible(true); + setSkeleton(true); + const data = await openCourseShow(course_id); + + if (data && Object.values(data)?.length) { + setCourseDetail(data); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setSkeleton(false); + }; + + // signup courses list + const handleSignupList = async (course: any) => { + course?.forEach((i: { id: number }) => params.append('course_Ids[]', String(i?.id))); + const data = await signupList(params); + if (data && data?.signed_courses) { + return data?.signed_courses; + } else { + return null; + } + }; + + // signUp + const сourseSignup = async (course_id: number) => { + setSignupDisebled(true); + const data = await openCourseSignup(course_id); + if (data?.success) { + handleSendSingup(); + const list: any | null = await handleSignupList(coursesValue); + if (list) { + setValueCourses((prev) => + prev.map((item) => ({ + ...item, + is_signed: list?.signed_courses?.includes(item.id) + })) + ); + setMessage({ + state: true, + value: { severity: 'success', summary: list?.message || translations.successAdd, detail: '' } + }); + setValueCourses((prev) => + prev.map((item) => ({ + ...item, + is_signed: list.signed_courses?.includes(item.id) + })) + ); + } + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setSignupDisebled(false); + }; + + const handleSendSingup = async () => { + const list: any | null = await handleSignupList(coursesValue); + if (list) setSignupList(list); + }; + + const clearFilter = () => { + setFree(null); + handleFetchOpenCourse(Number(page), '', '', null, null); + }; + + // Ручное управление пагинацией + const handlePageChange = (page: number) => { + router.push(`/openCourse/${page}`); + }; + + // category jsx + const categoryItemTemplate = (option: any) => { + return ( +
+ {getLocalizedName(option)} + {option.description && {option.description}} +
+ ); + }; + + const categoryValueTemplate = (option: any | null) => { + if (!option) { + return ...; + } + + return ( +
+ {getLocalizedName(option)} + {option.description && {option.description}} +
+ ); + }; + + // lang jsx + const langItemTemplate = (option: any) => { + return ( +
+
+ {option?.logo && flag} + {getLocalizedName(option)} +
+ {option.description && {option.description}} +
+ ); + }; + + const langValueTemplate = (option: any | null) => { + if (!option) { + return ...; + } + + return ( +
+ {option?.logo && flag} + {getLocalizedName(option)} +
+ ); + }; + + // filter render + const filterRender = () => ( +
+
+
+
+
+ +

{translations.free}

+
+
+ +

{translations.paid}

+
+
+
+
+
+
+
+ {translations.selectCategory} +
+ { + setCategorySelectedId(e.value); + // setPublicCategoryId(e.value?.id); + }} + options={depCategoryes} + optionLabel="name" + placeholder="..." + className="w-full text-sm" + /> +
+
+
+ {translations.selectLanguage} +
+ { + setLangSelectedId(e.value); + setLanguage_id(e.value?.id); + }} + options={depSelectLang} + optionLabel="name" + placeholder="..." + className="w-full text-sm" + /> +
+
+
+
+
+ ); + + // main content + const mainContent = () => { + if (skeleton) { + return ( + <> + + + + ); + } + + if (emptyCourse) { + return ( +
+ {translations.noCourses} +
+ ); + } + + return ( + <> +
+ {coursesValue?.map((item) => { + return ( +
+ {/* */} + +
+ ); + })} +
+
+ handlePageChange(e.page + 1)} + // template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink" + template={media ? 'FirstPageLink PrevPageLink NextPageLink LastPageLink' : 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'} + /> +
+ + ); + }; + + useEffect(() => { + setProgressSpinner(true); + if (search?.length === 0 && searchController) { + handleFetchOpenCourse(Number(page), free === 'paid' ? '3' : free === 'free' ? '2' : '', search, categorySelectedId?.id || null, language_id); + setSearchController(false); + setProgressSpinner(false); + } + + if (search?.length < 2) { + setProgressSpinner(false); + return; + } + + setSearchController(true); + const delay = setTimeout(() => { + handleFetchOpenCourse(Number(page), free === 'paid' ? '3' : free === 'free' ? '2' : '', search, categorySelectedId?.id || null, language_id); + setProgressSpinner(false); + }, 1000); + + return () => { + clearTimeout(delay); + }; + }, [search]); + + useEffect(() => { + if (categorySelectedId) handleFetchOpenCourse(Number(page), free === 'paid' ? '3' : free === 'free' ? '2' : '', search, categorySelectedId?.id || null, language_id); + }, [categorySelectedId]); + + useEffect(() => { + if (langSelectedId) handleFetchOpenCourse(Number(page), free === 'paid' ? '3' : free === 'free' ? '2' : '', search, categorySelectedId?.id || null, language_id); + }, [langSelectedId]); + + useEffect(() => { + if (coursesValue?.length) { + handleSendSingup(); + } + }, [coursesValue]); + + useEffect(() => { + handleFetchOpenCourse(Number(page), '', '', null, null); + handleDepCategoryFetch(); // получаем категории + handleDepLangFetch(); // получаем азыки + }, []); + + if (mainProgressSpinner) + return ( +
+ +
+ ); + + if (hasCourses) return ; + + return ( +
+
+ {/* header section */} + {translations.courses} + + {/* filter section */} + {filterRender()} +
+ + {/* courses section */} + {mainContent()} + + {/*{media && user?.is_student && }*/} + + setShowVisible(false)}> + {skeleton ? ( + + ) : courseDetail ? ( + + ) : ( + {translations.noData} + )} + +
+ ); +} diff --git a/app/(main)/openCourse/activeCourse/[course_id]/[lesson_id]/[step_id]/page.tsx b/app/(main)/openCourse/activeCourse/[course_id]/[lesson_id]/[step_id]/page.tsx new file mode 100644 index 00000000..55445018 --- /dev/null +++ b/app/(main)/openCourse/activeCourse/[course_id]/[lesson_id]/[step_id]/page.tsx @@ -0,0 +1,571 @@ +'use client'; + +import { NotFound } from '@/app/components/NotFound'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { statusView } from '@/services/notifications'; +import { fetchItemsLessons, fetchStudentSteps, fetchSubjects, stepPractica, stepTest } from '@/services/studentMain'; +import { docValueType } from '@/types/docValueType'; +import { lessonType } from '@/types/lessonType'; +import { mainStepsType } from '@/types/mainStepType'; +import { useParams } from 'next/navigation'; +import { Button } from 'primereact/button'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { useContext, useEffect, useState } from 'react'; +import { courseOpen, fetchActiveStepsDetail, openCoursePracticAdd, openCourseTestAdd } from '@/services/openCourse'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; + +export default function ActiveLessonDetail() { + // types + interface subjectType { + id_curricula: number; + course_ids: number[]; + streams: number[]; + } + + const { course_id, lesson_id, step_id } = useParams(); + + const media = useMediaQuery('(max-width: 640px)'); + const showError = useErrorMessage(); + const { setMessage } = useContext(LayoutContext); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + + const [steps, setSteps] = useState(null); + const [hasSteps, setHasSteps] = useState(false); + const [progressSpinner, setProgressSpinner] = useState(false); + const [type, setType] = useState(''); + const [practica, setPractica] = useState<{ + content?: { document: string; document_path: string; description: string | null; title: string; link: string; url: string; content: string; answers: [{ text: string; is_correct: boolean; id: number | null }]; score: number }; + } | null>(null); + const [test, setTests] = useState(null); + const [answer, setAnswer] = useState<{ id: number | null; text: string; is_correct: boolean }[] | null>(null); + const [selectedAnswer, setSelectedAnswer] = useState(false); + const [answerCheck, setAnswerCheck] = useState(false); + const [lessons, setLessons] = useState>({ + 1: { semester: { name_kg: '' } } + }); + const [lessonName, setLessonName] = useState(''); + const [courseInfo, setCoursesInfo] = useState<{ title: string; description: string; image: string } | null>(null); + const [main_id, setMain_id] = useState(null); + const [skeleton, setSkeleton] = useState(false); + const [courses, setCourses] = useState<{id: number;connections: { subject_type: string; id: number; user_id: number | null; id_stream: number }[];title: string;description: string;image: string;user: { last_name: string; name: string; father_name: string };lessons: lessonType[];} | null>(null); + const [docValue, setDocValue] = useState({ + title: '', + description: '', + file: null + }); + + // document + const [document, setDocument] = useState(null); + + // link + const [link, setLink] = useState(null); + + // video + const [video, setVideo] = useState(null); + const [preview, setPreview] = useState(false); + const [videoLink, setVideoLink] = useState(''); + + // Пуолчаем общие курсы + const handleCourseOpen = async () => { + const data = await courseOpen(course_id ? Number(course_id) : null); + if (data?.success) { + setCourses(data.course); + } + }; + + const handleSteps = async () => { + setSkeleton(true); + const data = await fetchActiveStepsDetail(course_id ? Number(course_id) : null, step_id ? Number(step_id) : null); + if (data?.success) { + setHasSteps(false); + // if (data?.courses?.length < 1) { + // setEmptyCourse(true); + // } else { + // setEmptyCourse(false); + // } + setSteps(data?.step); + } else { + setHasSteps(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setSkeleton(false); + }; + + const handleVideoCall = (value: string | null) => { + setPreview(true); + + if (!value) { + setPreview(true); + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при воспроизведении видео', detail: '' } + }); + } + + const url = new URL(typeof value === 'string' ? value : ''); + let videoId = null; + + if (url.hostname === 'youtu.be') { + // короткая ссылка, видео ID — в пути + videoId = url.pathname.slice(1); // убираем первый слеш + } else if (url.hostname === 'www.youtube.com' || url.hostname === 'youtube.com') { + // стандартная ссылка, видео ID в параметре v + videoId = url.searchParams.get('v'); + } + + if (!videoId) { + setPreview(true); + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при воспроизведении видео', detail: '' } + }); + return null; // не удалось получить ID + } + // return `https://www.youtube.com/embed/${videoId}`; + setVideoLink(`https://www.youtube.com/embed/${videoId}`); + setPreview(false); + // setVisisble(true); + }; + + const handleAddTest = async () => { + setProgressSpinner(true); + const isCorrect = answer?.filter((item) => item.is_correct); + const data = await openCourseTestAdd(Number(course_id), Number(step_id), isCorrect ? Number(isCorrect[0]?.id) : null); + + if (data?.success) { + setProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'success', summary: '', detail: data?.message } + }); + handleSteps(); + } else { + setProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при отправке ответа!', detail: '' } + }); + handleSteps(); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + const handleAddPractica = async () => { + setProgressSpinner(true); + const data = await openCoursePracticAdd(Number(course_id), Number(step_id), docValue.file); + if (data?.success) { + setProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'success', summary: '', detail: data?.message } + }); + handleSteps(); + } else { + setProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при отправке документа!', detail: '' } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + useEffect(() => { + if (steps?.type.name === 'document') { + setType(steps?.type.name); + setDocument(steps); + } else if (steps?.type.name === 'link') { + setType(steps?.type.name); + setLink(steps); + } else if (steps?.type.name === 'practical') { + setType(steps?.type.name); + setPractica(steps); + } else if (steps?.type.name === 'test') { + setType(steps?.type.name); + setTests(steps); + setAnswer(steps?.content?.answers || []); + } else if (steps?.type.name === 'video') { + setType(steps?.type.name); + setVideo(steps); + } + }, [steps]); + + useEffect(() => { + if (video?.content?.link) { + handleVideoCall(video.content.link); + } + }, [video]); + + useEffect(() => { + if (lesson_id) { + courses?.lessons?.forEach((j) => { + if (j?.id === Number(lesson_id)) { + setLessonName(j?.title || ''); + } + }); + + setCoursesInfo(courses || null); + } + }, [courses]); + + useEffect(() => { + const check = answer?.find((item) => item?.is_correct); + if (check) { + setAnswerCheck(true); + } else { + setAnswerCheck(false); + } + }, [answer]); + + useEffect(() => { + if (test?.answer_id && test?.answer_id != null) { + setSelectedAnswer(true); + } else { + setSelectedAnswer(false); + } + }, [test]); + + useEffect(() => { + handleCourseOpen(); + handleSteps(); + }, []); + + const docSection = ( +
+
+
+ {steps?.type?.title} + +
+
+ {document?.content?.title} + {document?.content?.description &&
{document?.content?.description &&
{document?.content?.description}
}
} +
+
+ {/* +
+
+
+ ); + + const linkSection = ( +
+
+
+ {steps?.type?.title} + +
+
+ {link?.content?.title} + {link?.content?.description &&
{link?.content?.description &&
{link?.content?.description}
}
} +
+
+ {translations.linkLabel}: + + {link?.content?.url} + +
+
+
+ ); + + const practicaSection = ( +
+
+
+ {steps?.type?.title} + +
+
+ Балл за задание: + {`${steps?.score}`} +
+
+ +
+ {practica?.content?.title} + +
+ {practica?.content?.description &&
} + +
+
+ {practica?.content?.document_path && practica?.content.document_path.toLowerCase().includes('pdf') && ( + <> + Документ: + + + )} +
+ +
+ {practica?.content?.url && ( +
+ {translations.linkLabel}: + {practica?.content.url && ( + + {practica?.content.url} + + )} +
+ )} +
+
+
+
+ + {/*
+ Сообщения от преподавателя + +
    +
  • Loremipsumdolorsitametconsecteturadipisicingelit. Mollitia, illum.
  • +
  • Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia, illum.
  • +
+
*/} + + {steps?.chills ? ( + Задание выполнено + ) : ( +
+ Задание после изучения материала, загрузи свой файл с решением. +
+ { + const file = e.target.files?.[0]; + if (file) { + const maxSize = 10 * 1024 * 1024; + + if (file.size > maxSize) { + setMessage({ + state: true, + value: { severity: 'error', summary: 'Файл слишком большой!', detail: 'Разрешено максимум 10 MB.' } + }); + } else { + setDocValue((prev) => ({ + ...prev, + file: file + })); + } + } + }} + /> +
+
+ {progressSpinner && } +
+
+ )} +
+ ); + + const testSection = ( +
+ {progressSpinner && ( +
+ +
+ )} +
+
+
+ {steps?.type?.title} + +
+
+ Балл за задание: + {`${steps?.score}`} +
+
+
+ {test?.content?.content} +
+ {test?.content?.answers.map((item, index) => { + return ( +
+ {selectedAnswer ? ( + <> + +
{item.text}
+ + ) : ( + <> + +
{item.text}
+ + )} +
+ ); + })} +
+
+
+ + {steps?.count_attempt && steps?.count_attempt >= 3 ? ( + Задание выполнено + ) : ( +
+
+ )} +
+ ); + + const videoSection = ( +
+
+
+ {steps?.type?.title} + +
+
+ {video?.content?.description && ( +
+ {video?.content?.title} + {video?.content?.description &&
{video?.content?.description &&
{video?.content?.description}
}
} +
+ )} +
+
+ {preview ? ( +
+
+ +
+ Видео +
+ ) : ( + + )} +
+
+
+ ); + + return ( +
+
+
+
0 ? 'justify-around flex-col sm:flex-row' : 'justify-center'}`}> +
+

+ {getLocalized(courseInfo, 'title') || courseInfo?.title} +

+
+

{translations.theme}:

+

{lessonName ? lessonName : '------'}

+
+ {getLocalized(courseInfo, 'description') || courseInfo?.description} +
+ {courseInfo?.image && courseInfo?.image.length > 0 && ( +
+ +
+ )} +
+
+
+ + {hasSteps && } + {type === 'document' && docSection} + {type === 'link' && linkSection} + {type === 'practical' && practicaSection} + {type === 'test' && testSection} + {type === 'video' && videoSection} +
+ ); +} diff --git a/app/(main)/openCourse/activeCourse/[course_id]/page.tsx b/app/(main)/openCourse/activeCourse/[course_id]/page.tsx new file mode 100644 index 00000000..4f45e3a5 --- /dev/null +++ b/app/(main)/openCourse/activeCourse/[course_id]/page.tsx @@ -0,0 +1,174 @@ +'use client'; + +import ActiveStepCard from '@/app/components/lessons/ActiveStepCard'; +import LessonInfoCard from '@/app/components/lessons/LessonInfoCard'; +import StudentInfoCard from '@/app/components/lessons/StudentInfoCard'; +import { NotFound } from '@/app/components/NotFound'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { courseOpen, fetchActiveSteps } from '@/services/openCourse'; +import { lessonStateType } from '@/types/lessonStateType'; +import { mainStepsType } from '@/types/mainStepType'; +import { myMainCourseType } from '@/types/myMainCourseType'; +import { useParams } from 'next/navigation'; +import { Accordion, AccordionTab } from 'primereact/accordion'; +import React, { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { BottomNav } from '@/app/components/menu/MobileMenu'; +import useMediaQuery from '@/hooks/useMediaQuery'; + +export default function ActiveCourseDetail() { + const { course_id } = useParams(); + + const { setMessage } = useContext(LayoutContext); + const showError = useErrorMessage(); + const { translations } = useLocalization(); + const [mainCourse, setMainCourses] = useState(null); + const [lessons, setLessonsValue] = useState([]); + const [emptyCourse, setEmptyCourse] = useState(false); + const [hasCourses, setHasCourses] = useState(false); + const [themeShow, setThemeShow] = useState(false); + const [skeleton, setSkeleton] = useState(false); + const [hasSteps, setHasSteps] = useState(false); + const [steps, setSteps] = useState([]); + const [activeIndex, setActiveIndex] = useState(0); + + const handleCourseOpen = async () => { + setSkeleton(true); + const data = await courseOpen(course_id ? Number(course_id) : null); + + if (data?.success) { + setHasCourses(false); + if (data?.courses?.length < 1) { + setEmptyCourse(true); + } else { + setEmptyCourse(false); + } + setMainCourses(data.course); + setLessonsValue(data.course?.lessons); + } else { + setHasCourses(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setSkeleton(false); + }; + + const handleSteps = async (lesson_id: number | null) => { + setSkeleton(true); + const data = await fetchActiveSteps(course_id ? Number(course_id) : null, lesson_id); + + if (data?.success) { + setHasSteps(false); + setSteps(data?.steps); + } else { + setHasSteps(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setSkeleton(false); + }; + + useEffect(() => { + handleCourseOpen(); + }, []); + + useEffect(() => { + if (lessons?.length > 0 && [activeIndex as number]) { + const lessonId = lessons[activeIndex as number]?.id; + if (lessonId) { + handleSteps(lessonId); + } + } + }, [lessons, activeIndex]); + + return ( +
+ {themeShow ? ( + + ) : ( +
+

+ {translations.courseName}: {mainCourse?.title} +

+ setActiveIndex(e.index)}> + {lessons?.map((item) => { + const content = steps.filter((j) => { + return j.content != null; + }); + + return ( + +
+ {hasSteps ? ( +

{translations.noData}

+ ) : content?.length > 0 ? ( + content.map((i, idx) => { + if (i.content) { + return ( +
+ { + handleTabChange(courses, course.id, accordionIndex)} + fetchProp={() => handleSteps(item?.id)} + // contentId={i?.content?.id} + // id_parent={i?.id_parent || null} + // forumValueAdd={() => { + // setForumValues({ description: i?.content.title || '', userInfo: { userName: course?.user?.name, userLastName: course?.user?.last_name } }); + // localStorage.setItem( + // 'forumValues', + // JSON.stringify({ description: i?.content.title || '', userInfo: { userName: course?.user?.name, userLastName: course?.user?.last_name } }) + // ); + // } + lessonItem={item} + stepItem={i} + /> + } +
+ ); + } + }) + ) : ( +

{translations.noData}

+ )} +
+
+ ); + })} +
+
+ )} +
+ ); +} diff --git a/app/(main)/openCourse/activeCourse/page.tsx b/app/(main)/openCourse/activeCourse/page.tsx new file mode 100644 index 00000000..e6d4dddf --- /dev/null +++ b/app/(main)/openCourse/activeCourse/page.tsx @@ -0,0 +1,217 @@ +'use client'; + +import MyDateTime from '@/app/components/MyDateTime'; +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { fetchActiveCourses } from '@/services/openCourse'; +import { CourseCategoryOption } from '@/types/openCourse/CourseCategoryOption'; +import { OptionsType } from '@/types/OptionsType'; +import Link from 'next/link'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import React, { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import MainTitle from '@/app/components/titles/MainTitle'; + +export default function ActiveCourseList() { + const { user, setMessage } = useContext(LayoutContext); + const showError = useErrorMessage(); + const media = useMediaQuery('(max-width: 640px)'); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + const [coursesValue, setValueCourses] = useState([]); + const [emptyCourse, setEmptyCourse] = useState(false); + const [hasCourses, setHasCourses] = useState(false); + const [skeleton, setSkeleton] = useState(false); + const [mainProgressSpinner, setMainProgressSpinner] = useState(false); + + const options: OptionsType = { + year: '2-digit', + month: 'short', // 'long', 'short', 'numeric' + day: '2-digit', + // hour: '2-digit', + // minute: '2-digit', + hour12: false // 24-часовой формат + }; + + const imageBodyTemplate = (product: any) => { + const image = product.image; + + if (typeof image === 'string') { + return ( +
+ Course image +
+ ); + } + + return ( +
+ Course image +
+ ); + }; + + function ProgressBar({ value = 0, max = 100, height = 'h-3', className = '' }) { + const safeMax = typeof max === 'number' && max > 0 ? max : 100; + const safeValue = typeof value === 'number' ? Math.max(0, Math.min(value, safeMax)) : 0; + const pct = (safeValue / safeMax) * 100; + + return ( +
+
+ +
+ ); + } + + const handleFetchActiveCourse = async () => { + setSkeleton(true); + setMainProgressSpinner(true); + const data = await fetchActiveCourses(); + + if (data?.success) { + setHasCourses(false); + + if (data?.courses?.length < 1) { + setEmptyCourse(true); + } else { + setEmptyCourse(false); + } + setValueCourses(data.courses); + } else { + setHasCourses(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setMainProgressSpinner(false); + setSkeleton(false); + }; + + const MyActiveCourse = ({ item }: { item: CourseCategoryOption }) => { + const link = { url: `/openCourse/activeCourse/${item?.id}`, status: true }; + + return ( +
+ {/* header section */} +
+
+ {imageBodyTemplate(item)} + {link.status ? ( + + {getLocalized(item, 'title') || item?.title} + + ) : ( + {getLocalized(item, 'title') || item?.title} + )} +
+
+
+ + {item?.audience_type?.name === 'open' ? translations.free : item?.audience_type?.name === 'wallet' ? translations.paid : ''} +
+
+
+ + {link.status ? ( + + {getLocalized(item, 'title') || item?.title} + + ) : ( + {getLocalized(item, 'title') || item?.title} + )} + + {/* score, progress */} +
+
+ {translations.score}: +
+ {item?.total_score || 0} / {item?.max_score?.total_score || 0} +
+
+ +
+
+ {translations.completionStatus} +
+
+
+ {Math.floor(typeof item?.progress_percent === 'number' ? item?.progress_percent : 0)} + % +
+
+ +
+
+
+
+ +
+
+ {/* data */} +
+ +
+
+
+ ); + }; + + useEffect(() => { + handleFetchActiveCourse(); + }, []); + + if (mainProgressSpinner) + return ( +
+ +
+ ); + + if (hasCourses) return ; + + return ( +
+ {translations.myActiveCourses} + + {skeleton ? ( + <> + + + + ) : emptyCourse ? ( + {translations.empty} + ) : ( +
+ {coursesValue?.map((item) => { + return ( +
+ +
+ ); + })} +
+ )} + + {/*{media && user?.is_student && }*/} +
+ ); +} diff --git a/app/(main)/openCourse/students/[course_id]/[id]/page.tsx b/app/(main)/openCourse/students/[course_id]/[id]/page.tsx new file mode 100644 index 00000000..696b0623 --- /dev/null +++ b/app/(main)/openCourse/students/[course_id]/[id]/page.tsx @@ -0,0 +1,94 @@ +'use client'; + +import React, { useState } from 'react'; +import { Accordion, AccordionTab } from 'primereact/accordion'; +import { CourseType } from '@/types/courseType'; +import { fetchCourseInfo } from '@/services/courses'; + +// Mock data as requested +const mockCourseData = { + title: 'Введение в веб-разработку: HTML, CSS и JavaScript', + teacher: { + name: 'Профессор Андрей Смирнов', + avatar: 'https://primefaces.org/cdn/primereact/images/avatar/amyelsner.png' + }, + lessons: [ + { + id: 1, + title: 'Тема 1: Основы HTML', + content: + '

В этом уроке мы изучим фундаментальные концепции HTML, структуру документа и основные теги для создания веб-страниц.

  • Структура HTML-документа
  • Основные теги: <h1>, <p>, <a>, <img>
  • Создание списков и таблиц
' + }, + { + id: 2, + title: 'Тема 2: Стилизация с помощью CSS', + content: + '

Узнайте, как придавать стиль вашим веб-страницам с помощью каскадных таблиц стилей (CSS). Мы рассмотрим селекторы, свойства и основы адаптивного дизайна.

Ключевые моменты: селекторы, свойства `color`, `font-size`, `margin`, `padding`, Flexbox.

' + }, + { + id: 3, + title: 'Тема 3: Введение в JavaScript', + content: '

Этот урок знакомит с основами JavaScript, делая ваши страницы интерактивными. Вы изучите переменные, типы данных, операторы и функции.

' + }, + { + id: 4, + title: 'Тема 4: Продвинутый JavaScript и DOM', + content: '

Погрузитесь глубже в JavaScript, научившись манипулировать объектной моделью документа (DOM) для динамического изменения содержимого и стиля веб-страниц.

' + } + ] +}; + +// A simple component to render lesson content safely +const LessonContent = ({ content }: { content: string }) => { + return
; +}; + +const StudentCheckPage = ({ params }: { params: { course_id: string; id: string } }) => { + const [mainSkeleton, mainSetSkeleton] = useState(false); + const [activeIndex, setActiveIndex] = useState(0); + const [courseShow, setCourseShow] = useState(null); + + const { title, teacher, lessons } = mockCourseData; + + // Получаем данные о курсе + const handleCourseShow = async () => { + mainSetSkeleton(true); + const data = await fetchCourseInfo(null); + if (data?.success) { + setCourseShow(data?.course); + } + mainSetSkeleton(false); + }; + + // Получаем уроки? + const handle = async () => { + mainSetSkeleton(true); + const data = await fetchCourseInfo(null); + if (data?.success) { + setCourseShow(data?.course); + } + mainSetSkeleton(false); + }; + + // const header = !courseShow && !courseShow?.title ?

{'courseShow?.title'}

: ''; + + return ( +
+
+ {/* {header} */} +
+

Темы Курса

+ setActiveIndex(e.index as number | null)}> + {lessons.map((lesson) => ( + + + + ))} + +
+
+
+ ); +}; + +export default StudentCheckPage; diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx deleted file mode 100644 index afe44527..00000000 --- a/app/(main)/page.tsx +++ /dev/null @@ -1,393 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -'use client'; -import { Button } from 'primereact/button'; -import { Chart } from 'primereact/chart'; -import { Column } from 'primereact/column'; -import { DataTable } from 'primereact/datatable'; -import { Menu } from 'primereact/menu'; -import React, { useContext, useEffect, useRef, useState } from 'react'; -import { ProductService } from '../../demo/service/ProductService'; -import { LayoutContext } from '../../layout/context/layoutcontext'; -import Link from 'next/link'; -import { Demo } from '@/types'; -import { ChartData, ChartOptions } from 'chart.js'; - -const lineData: ChartData = { - labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], - datasets: [ - { - label: 'First Dataset', - data: [65, 59, 80, 81, 56, 55, 40], - fill: false, - backgroundColor: '#2f4860', - borderColor: '#2f4860', - tension: 0.4 - }, - { - label: 'Second Dataset', - data: [28, 48, 40, 19, 86, 27, 90], - fill: false, - backgroundColor: '#00bb7e', - borderColor: '#00bb7e', - tension: 0.4 - } - ] -}; - -const Dashboard = () => { - const [products, setProducts] = useState([]); - const menu1 = useRef(null); - const menu2 = useRef(null); - const [lineOptions, setLineOptions] = useState({}); - const { layoutConfig } = useContext(LayoutContext); - - const applyLightTheme = () => { - const lineOptions: ChartOptions = { - plugins: { - legend: { - labels: { - color: '#495057' - } - } - }, - scales: { - x: { - ticks: { - color: '#495057' - }, - grid: { - color: '#ebedef' - } - }, - y: { - ticks: { - color: '#495057' - }, - grid: { - color: '#ebedef' - } - } - } - }; - - setLineOptions(lineOptions); - }; - - const applyDarkTheme = () => { - const lineOptions = { - plugins: { - legend: { - labels: { - color: '#ebedef' - } - } - }, - scales: { - x: { - ticks: { - color: '#ebedef' - }, - grid: { - color: 'rgba(160, 167, 181, .3)' - } - }, - y: { - ticks: { - color: '#ebedef' - }, - grid: { - color: 'rgba(160, 167, 181, .3)' - } - } - } - }; - - setLineOptions(lineOptions); - }; - - useEffect(() => { - ProductService.getProductsSmall().then((data) => setProducts(data)); - }, []); - - useEffect(() => { - if (layoutConfig.colorScheme === 'light') { - applyLightTheme(); - } else { - applyDarkTheme(); - } - }, [layoutConfig.colorScheme]); - - const formatCurrency = (value: number) => { - return value?.toLocaleString('en-US', { - style: 'currency', - currency: 'USD' - }); - }; - - return ( -
-
-
-
-
- Orders -
152
-
-
- -
-
- 24 new - since last visit -
-
-
-
-
-
- Revenue -
$2.100
-
-
- -
-
- %52+ - since last week -
-
-
-
-
-
- Customers -
28441
-
-
- -
-
- 520 - newly registered -
-
-
-
-
-
- Comments -
152 Unread
-
-
- -
-
- 85 - responded -
-
- -
-
-
Recent Sales
- - {data.image}} /> - - formatCurrency(data.price)} /> - ( - <> -
-
-
-
Best Selling Products
-
-
-
-
    -
  • -
    - Space T-Shirt -
    Clothing
    -
    -
    -
    -
    -
    - %50 -
    -
  • -
  • -
    - Portal Sticker -
    Accessories
    -
    -
    -
    -
    -
    - %16 -
    -
  • -
  • -
    - Supernova Sticker -
    Accessories
    -
    -
    -
    -
    -
    - %67 -
    -
  • -
  • -
    - Wonders Notebook -
    Office
    -
    -
    -
    -
    -
    - %35 -
    -
  • -
  • -
    - Mat Black Case -
    Accessories
    -
    -
    -
    -
    -
    - %75 -
    -
  • -
  • -
    - Robots T-Shirt -
    Clothing
    -
    -
    -
    -
    -
    - %40 -
    -
  • -
-
-
- -
-
-
Sales Overview
- -
- -
-
-
Notifications
-
-
-
- - TODAY -
    -
  • -
    - -
    - - Richard Jones - - {' '} - has purchased a blue t-shirt for 79$ - - -
  • -
  • -
    - -
    - - Your request for withdrawal of 2500$ has been initiated. - -
  • -
- - YESTERDAY -
    -
  • -
    - -
    - - Keyser Wick - - {' '} - has purchased a black jacket for 59$ - - -
  • -
  • -
    - -
    - - Jane Davis - has posted a new questions about your product. - -
  • -
-
-
-
-
TAKE THE NEXT STEP
-
Try PrimeBlocks
-
-
- - Get Started - -
-
-
-
- ); -}; - -export default Dashboard; diff --git a/app/(main)/pages/crud/page.tsx b/app/(main)/pages/crud/page.tsx deleted file mode 100644 index 52cf6e5e..00000000 --- a/app/(main)/pages/crud/page.tsx +++ /dev/null @@ -1,430 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -'use client'; -import { Button } from 'primereact/button'; -import { Column } from 'primereact/column'; -import { DataTable } from 'primereact/datatable'; -import { Dialog } from 'primereact/dialog'; -import { FileUpload } from 'primereact/fileupload'; -import { InputNumber, InputNumberValueChangeEvent } from 'primereact/inputnumber'; -import { InputText } from 'primereact/inputtext'; -import { InputTextarea } from 'primereact/inputtextarea'; -import { RadioButton, RadioButtonChangeEvent } from 'primereact/radiobutton'; -import { Rating } from 'primereact/rating'; -import { Toast } from 'primereact/toast'; -import { Toolbar } from 'primereact/toolbar'; -import { classNames } from 'primereact/utils'; -import React, { useEffect, useRef, useState } from 'react'; -import { ProductService } from '../../../../demo/service/ProductService'; -import { Demo } from '@/types'; - -/* @todo Used 'as any' for types here. Will fix in next version due to onSelectionChange event type issue. */ -const Crud = () => { - let emptyProduct: Demo.Product = { - id: '', - name: '', - image: '', - description: '', - category: '', - price: 0, - quantity: 0, - rating: 0, - inventoryStatus: 'INSTOCK' - }; - - const [products, setProducts] = useState(null); - const [productDialog, setProductDialog] = useState(false); - const [deleteProductDialog, setDeleteProductDialog] = useState(false); - const [deleteProductsDialog, setDeleteProductsDialog] = useState(false); - const [product, setProduct] = useState(emptyProduct); - const [selectedProducts, setSelectedProducts] = useState(null); - const [submitted, setSubmitted] = useState(false); - const [globalFilter, setGlobalFilter] = useState(''); - const toast = useRef(null); - const dt = useRef>(null); - - useEffect(() => { - ProductService.getProducts().then((data) => setProducts(data as any)); - }, []); - - const formatCurrency = (value: number) => { - return value.toLocaleString('en-US', { - style: 'currency', - currency: 'USD' - }); - }; - - const openNew = () => { - setProduct(emptyProduct); - setSubmitted(false); - setProductDialog(true); - }; - - const hideDialog = () => { - setSubmitted(false); - setProductDialog(false); - }; - - const hideDeleteProductDialog = () => { - setDeleteProductDialog(false); - }; - - const hideDeleteProductsDialog = () => { - setDeleteProductsDialog(false); - }; - - const saveProduct = () => { - setSubmitted(true); - - if (product.name.trim()) { - let _products = [...(products as any)]; - let _product = { ...product }; - if (product.id) { - const index = findIndexById(product.id); - - _products[index] = _product; - toast.current?.show({ - severity: 'success', - summary: 'Successful', - detail: 'Product Updated', - life: 3000 - }); - } else { - _product.id = createId(); - _product.image = 'product-placeholder.svg'; - _products.push(_product); - toast.current?.show({ - severity: 'success', - summary: 'Successful', - detail: 'Product Created', - life: 3000 - }); - } - - setProducts(_products as any); - setProductDialog(false); - setProduct(emptyProduct); - } - }; - - const editProduct = (product: Demo.Product) => { - setProduct({ ...product }); - setProductDialog(true); - }; - - const confirmDeleteProduct = (product: Demo.Product) => { - setProduct(product); - setDeleteProductDialog(true); - }; - - const deleteProduct = () => { - let _products = (products as any)?.filter((val: any) => val.id !== product.id); - setProducts(_products); - setDeleteProductDialog(false); - setProduct(emptyProduct); - toast.current?.show({ - severity: 'success', - summary: 'Successful', - detail: 'Product Deleted', - life: 3000 - }); - }; - - const findIndexById = (id: string) => { - let index = -1; - for (let i = 0; i < (products as any)?.length; i++) { - if ((products as any)[i].id === id) { - index = i; - break; - } - } - - return index; - }; - - const createId = () => { - let id = ''; - let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < 5; i++) { - id += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return id; - }; - - const exportCSV = () => { - dt.current?.exportCSV(); - }; - - const confirmDeleteSelected = () => { - setDeleteProductsDialog(true); - }; - - const deleteSelectedProducts = () => { - let _products = (products as any)?.filter((val: any) => !(selectedProducts as any)?.includes(val)); - setProducts(_products); - setDeleteProductsDialog(false); - setSelectedProducts(null); - toast.current?.show({ - severity: 'success', - summary: 'Successful', - detail: 'Products Deleted', - life: 3000 - }); - }; - - const onCategoryChange = (e: RadioButtonChangeEvent) => { - let _product = { ...product }; - _product['category'] = e.value; - setProduct(_product); - }; - - const onInputChange = (e: React.ChangeEvent, name: string) => { - const val = (e.target && e.target.value) || ''; - let _product = { ...product }; - _product[`${name}`] = val; - - setProduct(_product); - }; - - const onInputNumberChange = (e: InputNumberValueChangeEvent, name: string) => { - const val = e.value || 0; - let _product = { ...product }; - _product[`${name}`] = val; - - setProduct(_product); - }; - - const leftToolbarTemplate = () => { - return ( - -
-
-
- ); - }; - - const rightToolbarTemplate = () => { - return ( - - - - - ); - }; - - const customizedMarker = (item: CustomEvent) => { - return ( - - - - ); - }; - - return ( -
-
-
-
-
Left Align
- item.status} /> -
-
-
-
-
Right Align
- item.status} /> -
-
-
-
-
Alternate Align
- item.status} /> -
-
- -
-
-
Opposite Content
- item.status} content={(item) => {item.date}} /> -
-
- -
-
-
Customized
- -
-
-
-
-
Horizontal
-
Top Align
- item} /> - -
Bottom Align
- item} /> - -
Alternate Align
- item} opposite={ } /> -
-
-
-
- ); -}; - -export default TimelineDemo; diff --git a/app/(main)/pdf/[pdfUrl]/page.tsx b/app/(main)/pdf/[pdfUrl]/page.tsx new file mode 100644 index 00000000..1120a45e --- /dev/null +++ b/app/(main)/pdf/[pdfUrl]/page.tsx @@ -0,0 +1,23 @@ +'use client'; +import dynamic from 'next/dynamic'; + +const PDFreader = dynamic(() => import('@/app/components/pdfComponents/PDFreader'), { ssr: false }); + +import { useParams, useRouter } from 'next/navigation'; + +export default function PdfUrlViewer() { + const { pdfUrl } = useParams(); + const router = useRouter(); + + return ( +
+
+ +
+
+
+ ); +} diff --git a/app/(main)/roles/[page]/page.tsx b/app/(main)/roles/[page]/page.tsx new file mode 100644 index 00000000..380ad66c --- /dev/null +++ b/app/(main)/roles/[page]/page.tsx @@ -0,0 +1,677 @@ +'use client'; + +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { controlRolesUsers, fetchRolesList, fetchRolesUsers } from '@/services/roles/roles'; +import { RoleUserType } from '@/types/roles/RoleUserType'; +import { useParams } from 'next/navigation'; +import { useRouter } from 'next/navigation'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { DataTable } from 'primereact/datatable'; +import { Dialog } from 'primereact/dialog'; +import { Dropdown, DropdownChangeEvent } from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { Paginator } from 'primereact/paginator'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import React, { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; +import SubTitle from '@/app/components/titles/SubTitle'; +import MainTitle from '@/app/components/titles/MainTitle'; + +// types +interface Role { + id: number; + title: string; + created_at: string; + updated_at: string; + name_ru?: string; + name_kg?: string; +} + +interface Role_idType { + name: string; + role_id: number | null; +} + +interface RoleState { + create: boolean; + update: boolean; + delete: boolean; + show: boolean; +} + +export default function Roles() { + const { page } = useParams(); + const router = useRouter(); + + const showError = useErrorMessage(); + const { setMessage } = useContext(LayoutContext); + const media = useMediaQuery('(max-width: 640px)'); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + + const [userFetchGlag, setUserFetchGlag] = useState(false); + const [roles, setRoles] = useState(null); + const [users, setUsers] = useState(null); + + const [skeleton, setSkeleton] = useState(false); + const [contentNull, setContentNull] = useState(false); + const [mainProgressSpinner, setMainProgressSpinner] = useState(false); + const [miniProgressSpinner, setMiniProgressSpinner] = useState(false); + const [myeduProgressSpinner, setMyeduProgressSpinner] = useState(false); + const [search, setSearch] = useState(''); + const [searchController, setSearchController] = useState(false); + const [myeduController, setMyeduController] = useState(false); + const [active, setActive] = useState(false); + const [myedu_id, setMyedu_id] = useState(null); + const [pagination, setPagination] = useState<{ currentPage: number; total: number; perPage: number }>({ + currentPage: 1, + total: 0, + perPage: 0 + }); + // const [Number(page), setNumber(page)] = useState(1); + const [selectedRole_idType, setSelectedRole_idType] = useState({ name: translations.all, role_id: null }); + const [cities, setCities] = useState([{ name: translations.all, role_id: null }]); + const [forDisabled, setForDisabled] = useState(false); + const [categoryVisible, setCategoryVisible] = useState(false); + const [roleState, setRoleState] = useState({ + create: false, + update: false, + delete: false, + show: true + }); + const [role_id, setRole_id] = useState(null); + const [user_id, setUser_id] = useState(null); + + const handleFetchRoles = async () => { + setSkeleton(true); + setMainProgressSpinner(true); + const data = await fetchRolesList(); + if (data && Array.isArray(data)) { + if (data.length > 0) { + setContentNull(false); + setRoles(data); + setCities([{ name: translations.all, role_id: null }]); + const forSelectedRole_id: any = data?.map((item) => { + return { name: getLocalized(item, 'name') || item?.title, role_id: item?.id }; + }); + + if (forSelectedRole_id) setCities((prev) => [...prev, ...forSelectedRole_id]); + } else { + setContentNull(true); + } + } else { + setContentNull(false); + setMessage({ state: true, value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } }); + if (data?.response?.status) { + showError(data.response.status); + } + } + setSkeleton(false); + setMainProgressSpinner(false); + }; + + const handleFetchUsers = async (page: number, search: string, myedu_id: string | null, selectedRole_idType: Role_idType | null, active: boolean | null) => { + const res = await fetchRolesUsers(page, search, myedu_id, selectedRole_idType?.role_id ? selectedRole_idType?.role_id : null, active); + + if (res?.success) { + setPagination({ + currentPage: res?.data?.current_page, + total: res?.data?.total, + perPage: res?.data?.per_page + }); + const validRolesPosition = res?.data?.data?.map((item: any) => { + + if (item?.roles?.length > 1) { + // const [first, second] = item.roles; + // return { + // ...item, + // roles: [second, first] + // }; + const newRoles = [...item.roles]; + + // меняем местами первые два + [newRoles[0], newRoles[1]] = [newRoles[1], newRoles[0]]; + + return { + ...item, + roles: newRoles + }; + } + return item; + }); + if (validRolesPosition) { + setUsers(validRolesPosition); + } + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + }; + + const handleControlUsersRole = async (worker_id: number | null, selectedRole_idTypeParam: number | null, activeParam: boolean | null, roleState: RoleState) => { + clearValues(); + setForDisabled(true); + const res = await controlRolesUsers(worker_id, selectedRole_idTypeParam ? selectedRole_idTypeParam : null, activeParam, roleState); + if (res?.success) { + handleFetchRoles(); + setTimeout(() => { + handleFetchUsers(Number(page), search, myedu_id, selectedRole_idType, active); + }, 1000); + setMessage({ state: true, value: { severity: 'success', summary: translations.successChanged, detail: '' } }); + } else { + if (res.response.status === 400) { + setMessage({ state: true, value: { severity: 'error', summary: res.response.data.message, detail: '' } }); + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + } + setForDisabled(false); + }; + + const clearValues = () => { + setRole_id(null); + setUser_id(null); + setRoleState({ + create: false, + update: false, + delete: false, + show: true + }); + }; + + const checkRolesCrud = (user: any, role: number) => { + const userObj = user?.roles?.find((item: { id: number }) => item?.id === role); + if (userObj) { + setRoleState({ + create: userObj?.pivot?.create, + update: userObj?.pivot?.update, + delete: userObj?.pivot?.delete, + show: userObj?.pivot?.read + }); + } + }; + + // Ручное управление пагинацией + const handlePageChange = (page: number) => { + router.push(`/roles/${page}`); + }; + + // TSX + + const itemTemplate = (shablonData: any) => { + if (shablonData) { + return shablonData.map((item: any) => { + return ( +
+
+ + {item.last_name} {item.name} {item.father_name} + +
+ + {roles?.map((role, idx) => { + const userRole = item?.roles?.find((r: { id: number }) => r.id === role.id); + const isActive = Boolean(userRole?.pivot?.active); + return ( +
+ {/*{idx % 2 === 0 ? translations.administrator : translations.department}*/} + {role?.title} + +
+ {!isActive ? ( + + ) : ( + + )} +
+
+ ); + })} +
+ ); + }); + } + return []; + }; + + useEffect(() => { + setMiniProgressSpinner(true); + if (search?.length === 0 && searchController) { + handleFetchUsers(Number(page), search, myedu_id, selectedRole_idType, active); + setSearchController(false); + setMiniProgressSpinner(false); + } + + if (search?.length < 2) { + setMiniProgressSpinner(false); + return; + } + + setSearchController(true); + const delay = setTimeout(() => { + if (userFetchGlag) { + handleFetchUsers(Number(page), search, myedu_id, selectedRole_idType, active); + setMiniProgressSpinner(false); + } + }, 1000); + + return () => { + clearTimeout(delay); + }; + }, [search]); + + useEffect(() => { + if (selectedRole_idType && userFetchGlag) { + handleFetchUsers(Number(page), search, myedu_id, selectedRole_idType, active); + } + }, [selectedRole_idType]); + + useEffect(() => { + setMyeduProgressSpinner(true); + if (myedu_id?.length === 0 && myeduController) { + handleFetchUsers(Number(page), search, myedu_id, selectedRole_idType, active); + setMyeduController(false); + setMyeduProgressSpinner(false); + } + + if (myedu_id && myedu_id?.length < 4) { + setMyeduProgressSpinner(false); + return; + } + + setMyeduController(true); + const delay = setTimeout(() => { + if (userFetchGlag) { + handleFetchUsers(Number(page), search, myedu_id, selectedRole_idType, active); + } + setMyeduProgressSpinner(false); + }, 1000); + + return () => { + clearTimeout(delay); + }; + }, [myedu_id]); + + useEffect(() => { + handleFetchRoles(); + // handleFetchUsers(1, '', null, null, null); + setUserFetchGlag(true); + }, []); + + // Update default values when language changes + useEffect(() => { + setCities(prev => { + const newCities = [...prev]; + if (newCities.length > 0 && newCities[0].role_id === null) { + newCities[0].name = translations.all; + } + return newCities; + }); + + if (selectedRole_idType?.role_id === null) { + setSelectedRole_idType(prev => prev ? ({ ...prev, name: translations.all }) : null); + } + }, [translations]); + + const categoryFooterContent = ( +
+
+ ); + + if (mainProgressSpinner) + return ( +
+ +
+ ); + + if (contentNull) return ; + + return ( +
+ {skeleton ? ( + + ) : ( +
+
+ {translations.adminTitle} +
+
+
+ +

{translations.active}

+
+
+ setSelectedRole_idType(e.value)} options={cities} optionLabel="name" placeholder="..." className="w-[160px] sm:w-full text-sm" /> +
+
+
+ setMyedu_id(e.target.value)} /> +
{myeduProgressSpinner && }
+
+
+ +
+ setSearch(e.target.value)} className="w-full p-inputtext-sm p-inputtext-rounded" /> +
{miniProgressSpinner && }
+
+
+ + {/* main */} + {media ? ( + // +
{itemTemplate(users)}
+ ) : ( +
+ +
#
} body={(_, { rowIndex }) => rowIndex + 1} /> + +
{translations.fullName}
} + body={(rowData: any) => ( +
+ {rowData.last_name} {rowData.name} {rowData.father_name} +
+ )} + /> + {roles?.map((role, idx) => { + return ( +
{getLocalized(role, 'name') || role.title}
} + body={(user) => { + const userRole = user?.roles?.find((r: { id: number }) => r.id === role.id); + + const isActive = Boolean(userRole?.pivot?.active); + + return ( +
+
+ {!isActive ? ( + + ) : ( + + )} +
+
+ ); + }} + /> + ); + })} +
+ +
+ handlePageChange(e.page + 1)} + // template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink" + template={media ? 'FirstPageLink PrevPageLink NextPageLink LastPageLink' : 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'} + /> +
+
+ )} + { + if (!categoryVisible) return; + setCategoryVisible(false); + clearValues(); + }} + footer={categoryFooterContent} + > + { +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ } +
+
+ )} +
+ ); +} diff --git a/app/(main)/roles/departament/[page]/page.tsx b/app/(main)/roles/departament/[page]/page.tsx new file mode 100644 index 00000000..2e949db4 --- /dev/null +++ b/app/(main)/roles/departament/[page]/page.tsx @@ -0,0 +1,1313 @@ +'use client'; + +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { fetchCourseOpenStatus } from '@/services/courses'; +import { controlDepartamentUsers, depCategoryAdd, depCategoryDelete, depCategoryFetch, depCategoryShow, depCategoryUpdate, depLangFetch, fetchRolesDepartment, fetchTeacherCheck, teacherCoursePublic } from '@/services/roles/roles'; +import { CourseType } from '@/types/courseType'; +import { AudenceType } from '@/types/courseTypes/AudenceTypes'; +import { MainLangType } from '@/types/openCourse/MainLangType'; +import { TabViewChange } from '@/types/tabViewChange'; +import { getConfirmOptions } from '@/utils/getConfirmOptions'; +import Link from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { confirmDialog } from 'primereact/confirmdialog'; +import { DataTable } from 'primereact/datatable'; +import { Dialog } from 'primereact/dialog'; +import { Dropdown, DropdownChangeEvent } from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { Paginator } from 'primereact/paginator'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { TabPanel, TabView } from 'primereact/tabview'; +import React, { useContext, useEffect, useState } from 'react'; +import SubTitle from '@/app/components/titles/SubTitle'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; +import MainTitle from '@/app/components/titles/MainTitle'; + +// types +interface Role { + id: number; + title: string; + created_at: string; + updated_at: string; +} + +interface Role_idType { + name: string; + role_id: number | null; +} + +interface CategoryId { + title: string; + id: number | null; + description: string; +} + +interface TeacherCheckType extends CourseType { + course_audience_type_id: number | null; +} + +interface MainCategoryType extends CategoryId { + created_at: string; + updated_at: string; +} + +interface SelectLangType extends Pick {} + +export default function RolesDepartment() { + const { page } = useParams(); + const router = useRouter(); + + const showError = useErrorMessage(); + const { setMessage } = useContext(LayoutContext); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + + const media = useMediaQuery('(max-width: 640px)'); + + const [roleStatus, setRoleStatus] = useState<0 | 1>(0); + const [roles, setRoles] = useState(null); + + const [teachersCheck, setTeacherCheck] = useState(null); + + const [skeleton, setSkeleton] = useState(true); + const [contentNull, setContentNull] = useState(false); + const [mainProgressSpinner, setMainProgressSpinner] = useState(false); + const [miniProgressSpinner, setMiniProgressSpinner] = useState(false); + const [myeduProgressSpinner, setMyeduProgressSpinner] = useState(false); + const [search, setSearch] = useState(''); + const [searchController, setSearchController] = useState(false); + const [myeduController, setMyeduController] = useState(false); + const [active, setActive] = useState(false); + const [myedu_id, setMyedu_id] = useState(null); + const [pagination, setPagination] = useState<{ current_page: number; total: number; per_page: number }>({ + current_page: 1, + total: 0, + per_page: 0 + }); + const [checkPagination, setCheckPagination] = useState<{ current_page: number; total: number; per_page: number }>({ + current_page: 1, + total: 0, + per_page: 0 + }); + const [selectedTypeId, setSelectedTypeId] = useState({ name: translations.all, role_id: null }); + const [cities, setCities] = useState([{ name: translations.all, role_id: null }]); + + const [categorySelectedId, setCategorySelectedId] = useState({ title: translations.all, id: null, description: '' }); + const [depCategoryes, setCategoryes] = useState([{ title: translations.selectCategoryPlaceholder, id: null, description: '' }]); + + const [forDisabled, setForDisabled] = useState(false); + const [openTypes, setOpenTypes] = useState([]); + const [activeIndex, setActiveIndex] = useState(0); + const [comment, setPublicComment] = useState(null); + const [publicCourseId, setPublicCourseId] = useState(null); + const [publicStatus, setPublicStatus] = useState(null); + const [publicState, setPublicState] = useState(null); + const [visible, setVisible] = useState(false); + const [course_category_id, setPublicCategoryId] = useState(null); + + const [categoryState, setCategoryState] = useState(null); + const [categoryVisible, setCategoryVisible] = useState(false); + const [categoryValue, setCategoryValue] = useState({ title: '', description: '', id: null }); + const [cateroriesList, setCategoriesList] = useState(null); + + const [editingLesson, setEditingLesson] = useState(null); + const [checkOpenCourseEmpty, setCheckOpenCourseEmpty] = useState(false); + + const [langSelectedId, setLangSelectedId] = useState({ title: translations.all, id: null, description: '' }); + const [depSelectLang, setSelectLang] = useState([{ title: translations.selectCategoryPlaceholder, id: null, description: '' }]); + const [language_id, setLanguage_id] = useState(null); + + const [is_featured, setFeaturedChecked] = useState(false); + + const clearValues = () => { + setPublicCourseId(null); + setPublicState(null); + setPublicStatus(null); + setCategoryes([{ title: translations.selectCategoryPlaceholder, id: null, description: '' }]); + setCategorySelectedId({ title: translations.all, id: null, description: '' }); + setPublicCategoryId(null); + setLanguage_id(null); + setSelectLang([{ title: translations.selectCategoryPlaceholder, id: null, description: '' }]); + setLangSelectedId({ title: translations.all, id: null, description: '' }); + }; + + // fetch types + const handleFetchCourseOpenStatus = async () => { + const data = await fetchCourseOpenStatus(); + if (data && Array.isArray(data)) { + setOpenTypes(data); + setCities([{ name: translations.all, role_id: null }]); + const forSelectedRole_id: any = data?.map((item) => { + return { name: item?.title, role_id: item?.id }; + }); + setCities((prev) => [...prev, ...forSelectedRole_id]); + setContentNull(false); + } else { + setContentNull(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: '' } + }); // messege - Ошибка при изменении курса + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + // fetch roledepartements + const handleFetchDepartment = async (page: number, search: string, myedu_id: string | null, selectedTypeId: Role_idType | null, active: boolean | null) => { + setMainProgressSpinner(true); + const res = await fetchRolesDepartment(page, search, myedu_id, selectedTypeId?.role_id || null, active); + if (res?.success) { + setRoles(res?.data?.data); + // setPagination(res?.data); + setPagination({ + current_page: res?.data?.current_page, + total: res?.data?.total, + per_page: res?.data?.per_page + }); + setContentNull(false); + } else { + setContentNull(true); + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + setMainProgressSpinner(false); + setSkeleton(false); + }; + + const handleControlDepartament = async (worker_id: number, course_audience_type_id: number | null, activeParam: boolean | null) => { + // setForDisabled(true); + setMainProgressSpinner(true); + const res = await controlDepartamentUsers(worker_id, course_audience_type_id || null, activeParam); + if (res?.success) { + setTimeout(() => { + handleFetchDepartment(Number(page), search, myedu_id, selectedTypeId, active); + }, 1000); + setMessage({ state: true, value: { severity: 'success', summary: translations.successChanged, detail: '' } }); + } else { + setMainProgressSpinner(false); + if (res?.response?.status === 400) { + setMessage({ state: true, value: { severity: 'error', summary: res.response.data?.message, detail: '' } }); + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + } + // setForDisabled(false); + }; + + // checking functions + const handleFetchTeacherCheck = async (page: number | null, search: string | null, myedu_id: string | null, selectedTypeId: Role_idType | null) => { + setMainProgressSpinner(true); + const res = await fetchTeacherCheck(page, search, myedu_id, selectedTypeId?.role_id ? selectedTypeId?.role_id : null); + if (res?.success) { + if (res.data.data.length < 1) { + setCheckOpenCourseEmpty(true); + } else { + setTeacherCheck(res?.data?.data); + setCheckOpenCourseEmpty(false); + } + setCheckPagination(res?.data); + setContentNull(false); + } else { + setContentNull(true); + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + setMainProgressSpinner(false); + }; + + const handleCoursePublic = async (comment: string | null) => { + setMainProgressSpinner(true); + clearValues(); + const res = await teacherCoursePublic(Number(publicCourseId) || null, publicStatus, comment, course_category_id, language_id, is_featured); + if (res?.success) { + handleFetchTeacherCheck(Number(page), search, myedu_id, selectedTypeId); + setMessage({ state: true, value: { severity: 'success', summary: translations.successChanged, detail: '' } }); + } else { + if (res?.response?.status === 400) { + setMessage({ state: true, value: { severity: 'error', summary: res.response.data.message, detail: '' } }); + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + } + setMainProgressSpinner(false); + }; + + const editing = async (id: number) => { + setCategoryVisible(true); + setCategoryState(true); + const res = await depCategoryShow(id); + if (res && res?.id) { + setEditingLesson(res); + } + }; + + const handleDepCategoryFetch = async () => { + setMainProgressSpinner(true); + const res = await depCategoryFetch(); + if (res && res?.length) { + if (activeIndex === 1) { + handleFetchTeacherCheck(Number(page), search, myedu_id, selectedTypeId); + const forCategoryes = res.map((item: { title: string; id: number; description: string }) => { + return { name: item?.title, id: item?.id, description: item?.description }; + }); + if (forCategoryes) { + setCategoryes(forCategoryes); + } + } else if (activeIndex === 2) { + setCategoriesList(res); + } + } else { + if (res?.response?.status === 400) { + setMessage({ state: true, value: { severity: 'error', summary: res.response.data.message, detail: '' } }); + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + } + setMainProgressSpinner(false); + }; + + // add category + const handleCategoryAdd = async () => { + const res = await depCategoryAdd(categoryValue?.title || '', categoryValue?.description || ''); + if (res && res?.id) { + // handleFetchTeacherCheck(Number(checkPageState), search, myedu_id, selectedTypeId); + // const forCategoryes = res.map((item: { title: string, id: number, description: string }) => { + // return { name: item?.title, id: item?.id, description: item?.description }; + // }); + // if (forCategoryes) { + // setCategoryes(forCategoryes); + // } + handleDepCategoryFetch(); + setMessage({ state: true, value: { severity: 'success', summary: translations.successAdd, detail: '' } }); + } else { + if (res?.response?.status === 400) { + setMessage({ state: true, value: { severity: 'error', summary: res.response.data.message, detail: '' } }); + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + } + }; + + // delete + const handleCategoryDelete = async (id: number | null) => { + const res = await depCategoryDelete(id); + + if (res && res?.success) { + setMessage({ state: true, value: { severity: 'success', summary: translations.deleteSuccess, detail: '' } }); + handleDepCategoryFetch(); + } else { + if (res?.response?.status === 400) { + setMessage({ state: true, value: { severity: 'error', summary: res.response.data.message, detail: '' } }); + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + } + }; + + // updata + const handleCategoryUpdate = async () => { + const res = await depCategoryUpdate(editingLesson?.id || null, editingLesson?.title || '', editingLesson?.description || ''); + + if (res && res?.success) { + handleDepCategoryFetch(); + setMessage({ state: true, value: { severity: 'success', summary: translations.successChanged, detail: '' } }); + } else { + if (res?.response?.status === 400) { + setMessage({ state: true, value: { severity: 'error', summary: res.response.data.message, detail: '' } }); + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + } + }; + + // lang + const handleDepLangFetch = async () => { + const res = await depLangFetch(); + if (res && res?.success) { + if (activeIndex === 1) { + // handleFetchTeacherCheck(Number(checkPageState), search, myedu_id, selectedTypeId); + setSelectLang(res?.data); + } + } else { + if (res?.response?.status === 400) { + setMessage({ state: true, value: { severity: 'error', summary: res.response.data.message, detail: '' } }); + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } }); + if (res?.response?.status) { + showError(res.response.status); + } + } + } + }; + + // Ручное управление пагинацией + const handlePageChange = (page: number) => { + router.push(`/roles/departament/${page}`); + }; + + // Ручное управление пагинацией + const handleCheckPageChange = (page: number) => { + // handleFetchDepartment(page, search, myedu_id, selectedTypeId, active); + // handleFetchTeacherCheck(page, search, myedu_id, selectedTypeId); + // setCheckPageState(page); + router.push(`/roles/departament/check/${page}`); + }; + + // for tabview + const handleTabChange = (e: TabViewChange) => { + setActiveIndex(e.index); + if (e.index === 0 || e.index === 1) { + setSearch(''); + setMyedu_id(null); + setSelectedTypeId({ name: translations.all, role_id: null }); + setCities([{ name: translations.all, role_id: null }]); + setRoleStatus(e.index); + } + + if (e.index === 0) { + handleFetchCourseOpenStatus(); + } + }; + + // category jsx + const categoryItemTemplate = (option: any) => { + return ( +
+ {option.name} + {option.description && {option.description}} +
+ ); + }; + + const categoryValueTemplate = (option: any | null) => { + if (!option) { + return ...; + } + + return ( +
+ {option.name} + {option.description && {option.description}} +
+ ); + }; + + // lang jsx + const langItemTemplate = (option: any) => { + return ( +
+
+ flag + {option.title} +
+ {option.description && {option.description}} +
+ ); + }; + + const langValueTemplate = (option: any | null) => { + if (!option) { + return ...; + } + + return ( +
+ flag + {option.title} +
+ ); + }; + + // TSX access + const itemAccessTemplate = (roles: any) => { + if (roles) { + return roles.map((item: any) => { + return ( +
+
+ + {item.last_name} {item.name} {item.father_name} + +
+ + {openTypes?.map((role) => { + const element = item?.course_type_access.find((el: { id: number }) => el.id === role.id); + const isActive = Boolean(element?.pivot?.active); + + return ( +
+ {role?.title} + +
+
+ {!isActive && role?.id !== 1 ? ( + + ) : ( + + )} +
+
+
+ ); + })} +
+ ); + }); + } + }; + + const mainDepartamentSection = ( +
+ {media ? ( + itemAccessTemplate(roles) + ) : ( +
+ +
#
} body={(_, { rowIndex }) => rowIndex + 1} /> + +
{translations.fullName}
} + body={(rowData: any) => ( +
+ {rowData.last_name} {rowData.name} {rowData.father_name} +
+ )} + /> + + {openTypes?.map((item) => { + return ( + { + const element = rowData?.course_type_access.find((el: { id: number }) => el.id === item.id); + const isActive = Boolean(element?.pivot?.active); + + return ( +
+
+ {!isActive && item.id !== 1 ? ( + + ) : ( + + )} +
+
+ ); + }} + /> + ); + })} +
+
+ )} +
+ {/* handlePageChange(e.page + 1)}*/} + {/* template={media ? 'FirstPageLink PrevPageLink NextPageLink LastPageLink' : 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'}*/} + {/*/>*/} + handlePageChange(e.page + 1)} + template={media ? 'FirstPageLink PrevPageLink NextPageLink LastPageLink' : 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'} + /> +
+
+ ); + + // TSX checking + const itemCheckingTemplate = (roles: any) => { + if (roles) { + return roles.map((item: any) => { + return ( +
+
+ + {item?.user.last_name} {item?.user.name} {item?.user.father_name} + +
+ + {item.title} + +
+ + {item?.course_audience_type_id === 2 ? translations.openCourse : item?.course_audience_type_id === 3 ? translations.paid : ''} + +
+
+
+ + +
+
+ + +
+
+
+
+
+ ); + }); + } + }; + + const checkDepartamentSection = ( +
+ {media ? ( + itemCheckingTemplate(teachersCheck) + ) : ( +
+ {/* */} + {checkOpenCourseEmpty ? ( +

{translations.noData}

+ ) : ( + + rowIndex + 1} header="#"> + + ( + + {rowData.title} + + )} + > + + ( + + {rowData?.user.last_name} {rowData?.user.name} {rowData?.user.father_name} + + )} + > + + ( +
+ + {rowData?.course_audience_type_id === 2 ? translations.openCourse : rowData?.course_audience_type_id === 3 ? translations.paid : ''} + +
+ )} + >
+ + ( +
+ { + setPublicStatus(1); + setPublicState(true); + setPublicCourseId(rowData?.id); + setVisible(true); + handleDepCategoryFetch(); + handleDepLangFetch(); + }} + className="cursor-pointer pi pi-check text-[white] shadow rounded-full bg-[var(--mainColor)] p-[5px]" + style={{ fontSize: '0.813rem' }} + > + { + setPublicStatus(0); + setPublicState(false); + setPublicCourseId(rowData?.id); + setVisible(true); + }} + className="cursor-pointer pi pi-times text-[white] shadow rounded-full bg-[red] p-[5px]" + style={{ fontSize: '0.813rem' }} + > +
+ )} + >
+
+ )} +
+ )} + + {/*
*/} + {/* handleCheckPageChange(e.page + 1)}*/} + {/* // template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink"*/} + {/* template={media ? 'FirstPageLink PrevPageLink NextPageLink LastPageLink' : 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'}*/} + {/* />*/} + {/*
*/} +
+ ); + + // category crud + const categorySection = ( +
+ {/* {media ? ( + itemCheckingTemplate(teachersCheck) + ) : ( */} +
+

{translations.courseCategoryInfo}

+
+
+
+
+ {/* crud */} + + {/* table */} + {/* */} + + rowIndex + 1} header="#"> + + ( + + {rowData.title} + + )} + > + + ( +
+ { + editing(rowData?.id); + }} + > + { + const options = getConfirmOptions(Number(rowData.id), () => handleCategoryDelete(rowData.id)); + confirmDialog(options); + }} + > +
+ )} + >
+
+
+ {/* )} */} +
+ ); + + const footerContent = ( +
+
+ ); + + const categoryFooterContent = ( +
+
+ ); + + // USEEFFECTS + useEffect(() => { + if (roleStatus) { + handleFetchTeacherCheck(Number(page), search, myedu_id, selectedTypeId); + } + }, [roleStatus]); + + useEffect(() => { + if (teachersCheck && teachersCheck?.length > 0) { + const uniqAudenceIds = new Set(); + + for (const teacher of teachersCheck) { + const key: any = teacher?.course_audience_type_id; + if (key !== undefined) { + uniqAudenceIds.add(key); + } + } + const idsArray = Array.from(uniqAudenceIds); + if (idsArray?.length) { + const forCities: Role_idType[] = [{ name: translations.all, role_id: null }]; + for (const element of idsArray) { + const typeName: string = element === 2 ? translations.openCourse : element === 3 ? translations.paid : ''; + forCities.push({ name: typeName, role_id: element }); + } + setCities(forCities); + } + } + }, [teachersCheck]); + + useEffect(() => { + setMiniProgressSpinner(true); + if (search?.length === 0 && searchController) { + if (roleStatus) { + handleFetchTeacherCheck(Number(page), search, myedu_id, selectedTypeId); + } else { + handleFetchDepartment(Number(page), search, myedu_id, selectedTypeId, active); + } + setSearchController(false); + setMiniProgressSpinner(false); + } + + if (search?.length < 2) { + setMiniProgressSpinner(false); + return; + } + + setSearchController(true); + const delay = setTimeout(() => { + if (roleStatus) { + handleFetchTeacherCheck(Number(page), search, myedu_id, selectedTypeId); + } else { + handleFetchDepartment(Number(page), search, myedu_id, selectedTypeId, active); + } + setMiniProgressSpinner(false); + }, 1000); + + return () => { + clearTimeout(delay); + }; + }, [search]); + + useEffect(() => { + if (selectedTypeId) { + if (roleStatus) { + handleFetchTeacherCheck(Number(page), search, myedu_id, selectedTypeId); + } else { + handleFetchDepartment(Number(page), search, myedu_id, selectedTypeId, active); + } + } + }, [selectedTypeId]); + + useEffect(() => { + setMyeduProgressSpinner(true); + if (myedu_id?.length === 0 && myeduController) { + if (roleStatus) { + handleFetchTeacherCheck(Number(page), search, myedu_id, selectedTypeId); + } else { + handleFetchDepartment(Number(page), search, myedu_id, selectedTypeId, active); + } + + setMyeduController(false); + setMyeduProgressSpinner(false); + } + + if (myedu_id && myedu_id?.length < 4) { + setMyeduProgressSpinner(false); + return; + } + + setMyeduController(true); + const delay = setTimeout(() => { + if (roleStatus) { + handleFetchTeacherCheck(Number(page), search, myedu_id, selectedTypeId); + } else { + handleFetchDepartment(Number(page), search, myedu_id, selectedTypeId, active); + } + + setMyeduProgressSpinner(false); + }, 1000); + + return () => { + clearTimeout(delay); + }; + }, [myedu_id]); + + useEffect(() => { + if (activeIndex === 2) { + handleDepCategoryFetch(); + } + }, [activeIndex]); + + useEffect(() => { + handleFetchCourseOpenStatus(); + handleFetchDepartment(1, '', null, null, null); + }, []); + + // Update default values when language changes + useEffect(() => { + setCities(prev => { + const newCities = [...prev]; + if (newCities.length > 0 && newCities[0].role_id === null) { + newCities[0].name = translations.all; + } + return newCities; + }); + + if (selectedTypeId?.role_id === null) { + setSelectedTypeId(prev => prev ? ({ ...prev, name: translations.all }) : null); + } + + setCategoryes(prev => { + const newCats = [...prev]; + if (newCats.length > 0 && newCats[0].id === null) { + newCats[0].title = translations.selectCategoryPlaceholder; + } + return newCats; + }); + + if (categorySelectedId?.id === null) { + setCategorySelectedId(prev => prev ? ({ ...prev, title: translations.all }) : null); + } + + setSelectLang(prev => { + const newLangs = [...prev]; + if (newLangs.length > 0 && newLangs[0].id === null) { + newLangs[0].title = translations.selectCategoryPlaceholder; + } + return newLangs; + }); + + if (langSelectedId?.id === null) { + setLangSelectedId(prev => prev ? ({ ...prev, title: translations.all }) : null); + } + + }, [translations]); + + if (contentNull) return ; + + return ( +
+ {skeleton ? ( + + ) : ( +
+
+ {translations.department} +
+
+ {!roleStatus && ( +
+ +

{translations.active}

+
+ )} +
+ setSelectedTypeId(e.value)} options={cities} optionLabel="name" placeholder="..." className="w-[160px] sm:w-full text-sm" /> +
+
+
+ setMyedu_id(e.target.value)} /> +
{myeduProgressSpinner && }
+
+
+ +
+ setSearch(e.target.value)} className="w-full p-inputtext-sm p-inputtext-rounded" /> +
{miniProgressSpinner && }
+
+
+ + {/* main */} + + {mainProgressSpinner ? ( +
+ +
+ ) : ( + handleTabChange(e)} + activeIndex={activeIndex} + pt={{ + nav: { className: 'flex cursor-pointer px-2 pt-2' }, + panelContainer: { className: 'flex-1 pl-4' } + }} + > + {/* Departament */} + + {mainDepartamentSection} + + + {/* Checking */} + + {checkDepartamentSection} + + + {/* Category crud */} + + {categorySection} + + + )} +
+ )} + + {/* publising */} + { + if (!visible) return; + setVisible(false); + clearValues(); + }} + footer={footerContent} + > + { +
+ {/* Аннулирование */} + {publicState ? ( +
+
+ {translations.selectCourseCategory} +
+ { + setCategorySelectedId(e.value); + setPublicCategoryId(e.value?.id); + }} + options={depCategoryes} + optionLabel="name" + placeholder="..." + className="w-full text-sm" + /> +
+
+
+ {translations.selectCourseLanguage} +
+ { + setLangSelectedId(e.value); + setLanguage_id(e.value?.id); + }} + options={depSelectLang} + optionLabel="name" + placeholder="..." + className="w-full text-sm" + /> +
+
+
+ {translations.recommend} + +
+
+ ) : ( +
+ {translations.confirmRejectCourse} + setPublicComment(e.target.value)} type="text" placeholder={translations.rejectReason} /> +
+ )} +
+ } +
+ + {/* catetory */} + { + if (!categoryVisible) return; + setCategoryVisible(false); + setCategoryState(false); + clearValues(); + }} + footer={categoryFooterContent} + > + { +
+ {/* Аннулирование */} + {!categoryState ? ( +
+ { + setCategoryValue((prev) => ({ ...prev, title: e.target.value })); + }} + placeholder={translations.categoryName} + /> + { + setCategoryValue((prev) => ({ ...prev, description: e.target.value })); + }} + placeholder={translations.categoryDescription} + /> +
+ ) : ( +
+ { + setEditingLesson((prev) => prev && { ...prev, title: e.target.value }); + }} + placeholder={translations.categoryName} + /> + { + setEditingLesson((prev) => prev && { ...prev, description: e.target.value }); + }} + /> +
+ )} +
+ } +
+
+ ); +} diff --git a/app/(main)/roles/departament/check/[course_id]/page.tsx b/app/(main)/roles/departament/check/[course_id]/page.tsx new file mode 100644 index 00000000..0f62de0c --- /dev/null +++ b/app/(main)/roles/departament/check/[course_id]/page.tsx @@ -0,0 +1,211 @@ +'use client'; + +import LessonInfoCard from '@/app/components/lessons/LessonInfoCard'; +import { NotFound } from '@/app/components/NotFound'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { depExamination, depExaminationSteps } from '@/services/roles/roles'; +import { mainStepsType } from '@/types/mainStepType'; +import { useParams, useSearchParams } from 'next/navigation' +import { Accordion, AccordionTab } from 'primereact/accordion'; +import { Dialog } from 'primereact/dialog'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; + +export default function DepartamentChecking() { + const { course_id } = useParams(); + const { setMessage } = useContext(LayoutContext); + const showError = useErrorMessage(); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + + const [courseInfo, setCourseInfo] = useState<{ title: string } | null>(null); + const [themes, setThemes] = useState([]); + const [themeShow, setThemeShow] = useState(false); + const [hasSteps, setHasSteps] = useState(false); + const [steps, setSteps] = useState([]); + const [activeIndex, setActiveIndex] = useState(0); + const [videoCall, setVideoCall] = useState(false); + const [video_link, setVideoLink] = useState(''); + const [stepsSkeleton, setStepsSkeleton] = useState(false); + const [totalScore, setTotalScore] = useState(0); + + const handleFetchLessons = async () => { + const data = await depExamination(Number(course_id)); + if (data && data?.lessons) { + setThemeShow(false); + if (data.lessons?.length < 1) { + // setHasSteps(true); + } else { + // setHasSteps(false); + setThemes(data.lessons); + setCourseInfo({ title: data?.course?.title }); + setTotalScore(data?.max_sum_score); + } + } else { + setThemeShow(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const handleFetchSteps = async (lesson_id: number) => { + setStepsSkeleton(true); + const data = await depExaminationSteps(Number(lesson_id)); + if (data.success) { + if (data.steps?.length < 1) { + setHasSteps(true); + } else { + setHasSteps(false); + setSteps(data.steps); + } + } else { + setHasSteps(false); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + setStepsSkeleton(false); + }; + + const handleVideoCall = (value: string | null) => { + if (!value) { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: '' } + }); + } + + const url = new URL(typeof value === 'string' ? value : ''); + let videoId = null; + + if (url.hostname === 'youtu.be') { + // короткая ссылка, видео ID — в пути + videoId = url.pathname.slice(1); // убираем первый слеш + } else if (url.hostname === 'www.youtube.com' || url.hostname === 'youtube.com') { + // стандартная ссылка, видео ID в параметре v + videoId = url.searchParams.get('v'); + } + + if (!videoId) { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: '' } + }); + return null; // не удалось получить ID + } + // return `https://www.youtube.com/embed/${videoId}`; + setVideoLink(`https://www.youtube.com/embed/${videoId}`); + setVideoCall(true); + }; + + // ПРОСИМ КУРС ДЛЯ НАЗВАНИЯ И ТЕМЫ + useEffect(() => { + handleFetchLessons(); + }, []); + + // просто посмотреть пока + useEffect(() => { + if (themes?.length > 0 && [activeIndex as number]) { + const lessonId = themes[activeIndex as number]?.id; + if (lessonId) { + handleFetchSteps(lessonId); + } + } + }, [themes, activeIndex]); + + return ( +
+ { + if (!videoCall) return; + setVideoCall(false); + }} + > +
+ +
+
+ {themeShow ? ( + + ) : ( +
+
+

+ {translations.courseName}: {getLocalized(courseInfo, 'title') || courseInfo?.title} +

+ setActiveIndex(e.index)}> + {themes.map((item) => { + const content = steps.filter((j) => { + return j.content != null; + }); + + return ( + +
+ {stepsSkeleton ? ( + + ) : hasSteps ? ( +

{translations.noData}

+ ) : content?.length > 0 ? ( + content.map((i, idx) => { + if (i.content) { + return ( +
+ { + + } +
+ ); + } + }) + ) : ( +

{translations.noData}

+ )} +
+
+ ); + })} +
+
+
+ {translations.totalPointsForCourse}: + {totalScore} +
+
+ )} +
+ ); +} diff --git a/app/(main)/roles/scoreControl/page.tsx b/app/(main)/roles/scoreControl/page.tsx new file mode 100644 index 00000000..113dfb54 --- /dev/null +++ b/app/(main)/roles/scoreControl/page.tsx @@ -0,0 +1,355 @@ +'use client'; + +import React, { useState, useEffect, useCallback, useRef, useContext } from 'react'; +import { fetchCourseOpenStatus } from '@/services/courses'; +import { AudenceType } from '@/types/courseTypes/AudenceTypes'; +import { InputNumber } from 'primereact/inputnumber'; +import { Button } from 'primereact/button'; +import { Skeleton } from 'primereact/skeleton'; +import { classNames } from 'primereact/utils'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { Dialog } from 'primereact/dialog'; +import { InputText } from 'primereact/inputtext'; // Import InputText +import { InputTextarea } from 'primereact/inputtextarea'; +import { courseStatusControl } from '@/services/roles/roles'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { useRouter } from 'next/navigation'; // Import InputTextarea + +interface ForLinkRole { + name: string; + id: number | null; + active: boolean; + read: boolean; +} + +/** + * Компонент управления диапазоном баллов для конкретной строки + * Адаптирован для использования в модальном окне + */ +const ScoreRangeInputs = ({ min, max, onChange, disabled }: { min: number; max: number; onChange: (type: 'min' | 'max', val: number) => void; disabled?: boolean }) => { + return ( +
+
+ Мин + onChange('min', Number(e.value) || 0)} + min={0} + max={max} + inputClassName="w-3rem sm:w-4rem text-center p-1 text-sm border-none bg-transparent" + showButtons + buttonLayout="vertical" + incrementButtonClassName="hidden" + decrementButtonClassName="hidden" + disabled={disabled} + /> +
+
-
+
+ Макс + onChange('max', Number(e.value) || 0)} + min={min} + inputClassName="w-3rem sm:w-4rem text-center p-1 text-sm border-none bg-transparent" + showButtons + buttonLayout="vertical" + incrementButtonClassName="hidden" + decrementButtonClassName="hidden" + disabled={disabled} + /> +
+
+ ); +}; + +/** + * Компонент строки таблицы (или карточки на мобилке) + */ +const ScoreRow = ({ item, typesFetchProp }: { item: AudenceType; typesFetchProp: () => void }) => { + const { setMessage } = useContext(LayoutContext); + const [displayEditModal, setDisplayEditModal] = useState(false); + const [editableScores, setEditableScores] = useState({ min: item.min_score, max: item.max_score }); + const [editableTitle, setEditableTitle] = useState(item.title); + const [editableDescription, setEditableDescription] = useState(item.description); + const [loading, setLoading] = useState(false); + const {translations} = useLocalization(); + + const handleScoreChange = (type: 'min' | 'max', val: number) => { + if (typeof val === 'number') { + setEditableScores((prev) => ({ ...prev, [type]: val })); + } + }; + + const handleSaveScores = async () => { + setLoading(true); + // Simulate API call + const data = await courseStatusControl(editableTitle, editableDescription, editableScores.min, editableScores.max, item.id); + if(data){ + typesFetchProp(); // Refresh data after saving + setMessage({ + state: true, + value: { severity: 'success', summary: translations.updateSuccess, detail: '' } + }); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data.response.data.cause } + }); + } + console.log(`Saving for ${item.id}:`, { + min_score: editableScores.min, + max_score: editableScores.max, + title: editableTitle, + description: editableDescription + }); + // In a real application, you would make an API call here to update the scores, title, and description + // e.g., updateCourseDetails(item.id, editableScores.min, editableScores.max, editableTitle, editableDescription); + setLoading(false); + setDisplayEditModal(false); + }; + + // const handleDelete = (id: number)=> { + // console.log(id); + // } + + useEffect(() => { + setEditableScores({ min: item.min_score, max: item.max_score }); + setEditableTitle(item.title); + setEditableDescription(item.description); + }, [item.min_score, item.max_score, item.title, item.description]); + + const isChanged = + Number(editableScores.min) !== Number(item.min_score) || + Number(editableScores.max) !== Number(item.max_score) || + editableTitle !== item.title || + editableDescription !== item.description; + + return ( + + +
+
+
+ +
+ + {item.title} + +
+

{item.description}

+
+ + +
+
+
+ Мин + {item.min_score} +
+
-
+
+ Макс + {item.max_score} +
+
+ setDisplayEditModal(true)}> + {/* {*/} + {/* confirmDialog(getConfirmOptions(Number(), () => handleDelete(item.id)));*/} + {/*}}>*/} +
+ + + setDisplayEditModal(false)} + footer={ +
+
+ } + > +
+
+ + setEditableTitle(e.target.value)} + disabled={loading} + className="w-full" + /> +
+ +
+ + setEditableDescription(e.target.value)} + rows={3} + cols={30} + disabled={loading} + className="w-full" + /> +
+ +
+ + +
+
+
+ + ); +}; + +/** + * Заглушка загрузки + */ +const TableSkeleton = () => ( + <> + {[1, 2, 3, 4].map((i) => ( + + + + + + +
+ + +
+ + + ))} + +); + +/** + * Основной компонент страницы + */ +export default function ScoreControl() { + const {user} = useContext(LayoutContext); + const { translations } = useLocalization(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [scoreControlInfo, setScoreControlInfo] = useState(false); + const router = useRouter(); + + const handleFetchTypes = async () => { + try { + const res = await fetchCourseOpenStatus(); + setData(res); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + handleFetchTypes(); + + const roles = user?.roles; + const forRole: ForLinkRole[] = []; + if(roles){ + roles.forEach((role) => { + if (role?.pivot?.active) { + const timeRole: ForLinkRole = { + name: role.title, + id: role.id, + active: true, + read: role?.pivot?.read + }; + + forRole.push(timeRole); + } + }); + const forScoreControl = forRole?.find((item) => item.id === 5); + if(!forScoreControl) { + router.push('/'); + } + } else { + router.push('/'); + } + console.log(roles); + }, []); + + return ( +
+
+
+
+

{translations.scoreControle}

+ setScoreControlInfo(true)} className={'cursor-pointer pi pi-info-circle text-lg text-[var(--titleColor)]'}> +
+ +
+ + + + + + + + {loading ? : data.map((item) => )} +
{translations.courseAudienceType}{translations.scoreDiapazon}
+
+ + {!loading && data.length === 0 && ( +
+ +

Данные не найдены

+
+ )} +
+
+ + { + if (!scoreControlInfo) return; + setScoreControlInfo(false); + }} + > +
Здесь вы можете гибко настроить диапазон баллов (минимум и максимум) для каждого типа курса
+
+ + +
+ ); +} diff --git a/app/(main)/roles/students/[page]/[id_student]/page.tsx b/app/(main)/roles/students/[page]/[id_student]/page.tsx new file mode 100644 index 00000000..d9a98049 --- /dev/null +++ b/app/(main)/roles/students/[page]/[id_student]/page.tsx @@ -0,0 +1,448 @@ +'use client'; +import React, { useState, useEffect, useContext } from 'react'; +import { Accordion, AccordionTab } from 'primereact/accordion'; +import { Button } from 'primereact/button'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { Message } from 'primereact/message'; +import { Dialog } from 'primereact/dialog'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { fetchStudentData, studentCancel } from '@/services/roles/roles'; +import { useParams } from 'next/navigation'; +import { InputText } from 'primereact/inputtext'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { fetchStudentSearchDetail } from '@/services/student/studentSearch'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import { TabPanel, TabView } from 'primereact/tabview'; +import { TabViewChange } from '@/types/tabViewChange'; +import CoursesCut from '@/app/components/tables/coursesCut'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +// Типизация данных (можно вынести в отдельные файлы в /types) +interface Student { + id: string; + name: string; + last_name: string; + father_name: string; + email: string; + avatar: string; + faculty: string; +} + +interface Step { + id: string; + type: 'lesson' | 'test' | 'assignment'; + title: string; + completed: boolean; +} + +interface Course { + id: string; + title: string; + progress: number; + steps: Step[]; +} + +const StudentDetailPage = ({ params }: { params: { student_id: string } }) => { + const { id_student } = useParams(); + const { setMessage } = useContext(LayoutContext); + const { translations } = useLocalization(); + const showError = useErrorMessage(); + + const [student, setStudent] = useState(null); + const [courses, setCourses] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [answer_ids, setAnswerIds] = useState([]); + const [currentCourseId, setCurrentCourseId] = useState(null); + const [dialogVisible, setDialogVisible] = useState(false); + const [instructVisible, setInstructVisible] = useState(false); + const [annulmentReason, setAnnulmentReason] = useState(''); + const [description, setDescription] = useState(''); + const [skeleton, setSkeleton] = useState(false); + const [activeIndex, setActiveIndex] = useState(0); + const [fakeCheck, setFakeCheck] = useState(false); + const [accrodionIndex, setAccrodionIndex] = useState(0); + + // --- Шаблон для запроса данных --- + const handleFetchStudentData = async () => { + setLoading(true); + setError(null); + const data = await fetchStudentData(Number(id_student)); + if (data && Array.isArray(data)) { + setCourses(data); + } else { + setError(true); + } + setLoading(false); + }; + + const handleTabChange = (e: TabViewChange) => { + setActiveIndex(e.index); + }; + + const handleFetchStudentDetail = async () => { + setSkeleton(true); + const data = await fetchStudentSearchDetail(Number(id_student)); + + if (data?.success) { + setStudent(data?.student); + } + setSkeleton(false); + }; + + const handlestudentCancel = async () => { + setLoading(true); + setError(null); + setDescription(''); + setAnnulmentReason(''); + + const data = await studentCancel(false, Number(currentCourseId), annulmentReason, answer_ids, Number(id_student), description); + if (data) { + setAnswerIds([]); + handleFetchStudentData(); + setMessage({ + state: true, + value: { severity: 'success', summary: data?.message, detail: '' } + }); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + + setLoading(false); + }; + + useEffect(() => { + handleFetchStudentData(); + }, [params.student_id]); + + useEffect(() => { + handleFetchStudentDetail(); + }, []); + + // Рендер контента + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ; + } + + const StudentInfo = () => { + return ( +
+
+
+ {/* ФИО Студента */} +
+

+ {student?.last_name} {student?.name} {student?.father_name} +

+
+
+
+
+ ); + }; + + return ( +
+ {skeleton ? : } + + <> + handleTabChange(e)} + activeIndex={activeIndex} + // className="main-bg" + pt={{ + nav: { className: 'flex flex-wrap text-sm' }, + panelContainer: { className: 'flex-1 pl-4' } + }} + > + + {courses.length === 0 ? ( + {translations.studentNotCourse} + ) : ( +
+ { + setAccrodionIndex(e.index); + setFakeCheck(false); + setCurrentCourseId(null); + setAnswerIds([]); + }} + > + {courses.map((course: any, idx) => ( + + + {idx + 1}. Курс: {course.title} + + + } + className={`w-full p-accordion my-accardion-icon`} + style={{ width: '100%', backgroundColor: 'white' }} + > +
+ {course?.lesson_step_answers.length > 0 ? ( + <> +
+ {/* Предмет */} +
+

Предмет

+
+ +

{course?.subject?.name_ru}

+
+
+
+ + +
+
+
+ {course.lesson_step_answers.map((step: any) => ( +
+ {step?.test && ( +
+
+ {fakeCheck ? ( + <> + + + ) : ( + <> + + + )} +
+
+ +
+ {step?.test?.content || 'Тест'} +
+
+
+ Балл: {step?.test?.score} +
+
+ )} + + {step?.practical && ( +
+
+ {fakeCheck ? ( + <> + + + ) : ( + <> + + + )} +
+
+ +
+ {step?.practical?.title || 'Практическая работа'} +
+
+
+ Балл: {step?.practical?.score} +
+
+ )} +
+ ))} +
+ + ) : ( +
+ +
+ )} +
+
+ ))} +
+
+
+
+ )} +
+ + +
+
setInstructVisible(true)} className="cursor-pointer flex items-center gap-1 justify-end text-[var(--mainColor)] "> + + Инструкция +
+ +
+
+
+ + setDialogVisible(false)} + footer={ +
+
+ } + > +
+
+ + setAnnulmentReason(e.target.value)} /> + setDescription(e.target.value)} rows={5} /> +
+
+
+ + setInstructVisible(false)}> +
+

+ {translations.studentWorkCencalled} +

+
+
+ +
+ ); +}; + +export default StudentDetailPage; diff --git a/app/(main)/roles/students/[page]/reductor/page.tsx b/app/(main)/roles/students/[page]/reductor/page.tsx new file mode 100644 index 00000000..23586986 --- /dev/null +++ b/app/(main)/roles/students/[page]/reductor/page.tsx @@ -0,0 +1,410 @@ +'use client'; + +import React, { useContext, useEffect, useState, useRef } from 'react'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import 'primereact/resources/themes/lara-light-blue/theme.css'; +import 'primereact/resources/primereact.min.css'; +import 'primeicons/primeicons.css'; +import Link from 'next/link'; +import { fethcReductor } from '@/services/roles/roles'; +import { RoleUserType } from '@/types/roles/RoleUserType'; +import { Paginator } from 'primereact/paginator'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { NotFound } from '@/app/components/NotFound'; +import { fetchFaculty } from '@/services/faculty'; +import { fetchSpeciality } from '@/services/student/studentSearch'; +import { Dropdown } from 'primereact/dropdown'; +import { useParams, useRouter, useSearchParams } from 'next/navigation'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +interface Student extends RoleUserType { + speciality: { + name_ru: string; + } | null; +} + +export default function StudentsPage() { + const { contextFilterState, setContextFilterState } = useContext(LayoutContext); + const { translations } = useLocalization(); + const media = useMediaQuery('(max-width: 640px)'); + const { page } = useParams<{page: string}>(); + const queryParams = useSearchParams(); + const searchParams = queryParams.get('search'); + + const [students, setStudents] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [pagination, setPagination] = useState<{ currentPage: number; total: number; perPage: number }>({ + currentPage: 1, + total: 0, + perPage: 0 + }); + const [search, setSearch] = useState(searchParams || ''); + const [progressSpinner, setProgressSpinner] = useState(false); + const [searchController, setSearchController] = useState(false); + const [empty, setEmpty] = useState(false); + const [hideInstruction, setHideInstructon] = useState(); + const [timeMode, setTimeMode] = useState<{ name_ru: string; code: number | null; id: number | null } | null>({ name_ru: '', code: null, id: null }); + const [timeModeOptions, setTimeModeOptions] = useState(null); + + const isFirstRender = useRef(true); + const prevSearch = useRef(search); + + const [speciality, setSpecialyty] = useState<{ name_ru: string; code: number | null; id: number | null } | null>(null); + const [specialityOptions, setSpecialityOptions] = useState(null); + + const [currentFacultyId, setCurrentFacultyId] = useState(null); + const [currentSpecialityId, setCurrentSpecialityId] = useState(null); + + const router = useRouter(); + + const handleFetchFaculty = async () => { + const data = await fetchFaculty(); + if (data && data?.length) { + const alls = { name_ru: 'Все', code: null, id: null }; + data.unshift(alls); + setTimeModeOptions(data); + } + }; + + const handleStudentSpeciality = async (id_faculty: number) => { + const data = await fetchSpeciality(id_faculty); + if (data && data?.length) { + const alls = { name_ru: 'Все', code: null, id: null }; + data.unshift(alls); + setSpecialityOptions(data); + // setStudents(data?.data); + } + }; + + // Асинхронная функция для будущего запроса + const handleFetchReductor = async (page: number, search: string, specialityId: number | null) => { + setLoading(true); + setError(null); + const data = await fethcReductor(page, search, specialityId); + if (data && data?.current_page) { + setPagination({ + currentPage: data?.current_page, + total: data?.total, + perPage: data?.per_page + }); + if (data?.data?.length > 0) { + setStudents(data?.data); + setEmpty(false); + } else { + setEmpty(true); + } + } else { + setError(true); + } + setLoading(false); + }; + + // Ручное управление пагинацией + const handlePageChange = (page: number) => { + router.push(`/roles/students/${page}/reductor?${search ? `search=${search}` : ''}`); + }; + + const studentsMobile = (student: any) => ( +
+
+
+
+
+ + {student?.last_name} + {student?.name} + {student?.father_name} + +
+ {student.myedu_id} +
+ +
+ Специальность: + {student.speciality ? student.speciality?.name_ru : ''} +
+ + + + +
+
+
+ ); + + function EditorPanel() { + return ( +
+ {/* --- МОБИЛЬНАЯ ВЕРСИЯ (Список карточек) --- */} +
+
{students.map((student) => studentsMobile(student))}
+
+ + {empty ? ( + {translations.studentsNotFound} + ) : loading ? ( +
+ +
+ ) : ( + <> +
+ + + + + + + + + + + {students.map((student) => ( + + + + + + + ))} + +
{translations.fullNameStudent}{translations.personalShortNumber}{translations.speciality}{translations.actions}
+ + {student?.last_name} + {student?.name} + {student?.father_name} + + + {student.myedu_id} + {student.speciality ? student.speciality?.name_ru : ''} + + + +
+
+ {/* Футер таблицы */} +

+ {translations.viewStudents}: {students.length} {translations.from} {students.length} +

+ + handlePageChange(e.page + 1)} + // template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink" + template={media ? 'FirstPageLink PrevPageLink NextPageLink LastPageLink' : 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'} + /> + + )} +
+ ); + } + + const instructionSection = ( +
+ +
+

{translations.reductorInstruction}

+

{translations.reductorWorkingInstruction}

+
+ { + localStorage.setItem('hideInstruction', 'true'); + setHideInstructon(false); + }} + className="pi pi-times cursor-pointer" + > +
+ ); + + const facultiItemeTemplate = (option: any) => { + return ( +
+ {option.name_ru} +
+ ); + }; + + const specialityItemeTemplate = (option: any) => { + return ( +
+ {option.name_ru} +
+ ); + }; + + useEffect(() => { + // Пропускаем первый рендер + if (isFirstRender.current) { + isFirstRender.current = false; + prevSearch.current = search; + return; + } + + // Пропускаем если search не изменился + if (prevSearch.current === search) { + return; + } + + prevSearch.current = search; + + // Сброс при пустой строке + if (!search || search.length === 0) { + if (searchController) { + handleFetchReductor(1, search, currentSpecialityId); + setSearchController(false); + } + setProgressSpinner(false); + return; + } + + if (search.length < 2) { + setProgressSpinner(false); + return; + } + + // Дебоунс запрос + setProgressSpinner(true); + setSearchController(true); + + const delay = setTimeout(() => { + handleFetchReductor(1, search, currentSpecialityId); + setProgressSpinner(false); + }, 1000); + + return () => clearTimeout(delay); + }, [search]); + + useEffect(() => { + if (timeMode?.id) { + setCurrentFacultyId(timeMode?.id); + setContextFilterState((prev: any) => ({ ...prev, faculty_id: timeMode?.id })); + } + }, [timeMode]); + + useEffect(() => { + if (contextFilterState?.faculty_id && timeModeOptions?.length) { + const selected = timeModeOptions.find((item: any) => item.id === contextFilterState.faculty_id); + + if (selected) { + setTimeMode(selected); + } + } + }, [timeModeOptions, contextFilterState?.faculty_id]); + + useEffect(() => { + if (currentFacultyId) { + handleStudentSpeciality(currentFacultyId); + } + }, [currentFacultyId]); + + useEffect(() => { + if (speciality && speciality.id !== currentSpecialityId) { + setCurrentSpecialityId(speciality.id); + handleFetchReductor(Number(page), search, speciality.id); + } + }, [speciality]); + + useEffect(() => { + if (specialityOptions?.length && contextFilterState?.speciality_id !== undefined) { + const selected = specialityOptions.find((item: any) => item.id === contextFilterState.speciality_id); + + if (selected) { + setSpecialyty(selected); + } + } + }, [specialityOptions, contextFilterState?.speciality_id]); + + useEffect(() => { + handleFetchReductor(Number(page), search, currentSpecialityId); + handleFetchFaculty(); + const hide = localStorage.getItem('hideInstruction'); + if (hide) setHideInstructon(false); + else setHideInstructon(true); + }, []); + + return ( +
+
+ {/* Заголовок */} +
+
+ +
+
+

{translations.reductorPanel}

+

{translations.studentControl}

+
+
+ + {/* Блок инструкции */} + {hideInstruction && instructionSection} + + {/* filter */} +
+
+
+ {translations.selectFaculty} +
+ setTimeMode(e.value)} + placeholder={translations.selectFaculty} + className="text-wrap word-break sm:text-nowrap sm:max-w-full" + /> +
+
+ +
+ {translations.selectSpeciality} +
+ { + setSpecialyty(e.value); + setContextFilterState((prev: any) => ({ + ...prev, + speciality_id: e.value?.id ?? null + })); + }} + placeholder={translations.selectSpeciality} + className={`${!specialityOptions ? 'pointer-events-none opacity-50' : ''} w-full text-sm`} + /> +
+
+
+
+ + {/* Поиск */} +
+ + setSearch(e.target.value)} type="text" placeholder={translations.searchByFullName} className="w-full pr-4 outline-none" /> +
{progressSpinner && }
+
+ + {error ? ( +
+ +
+ ) : ( + + )} +
+
+ ); +} diff --git a/app/(main)/roles/teacherCheck/page.tsx b/app/(main)/roles/teacherCheck/page.tsx new file mode 100644 index 00000000..7ab5371a --- /dev/null +++ b/app/(main)/roles/teacherCheck/page.tsx @@ -0,0 +1,317 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { fetchAnwerReport } from '@/services/studentMain'; +import { myMainCourseType } from '@/types/myMainCourseType'; +import { Paginator } from 'primereact/paginator'; +import { fetchSpeciality } from '@/services/student/studentSearch'; +import { fetchFaculty } from '@/services/faculty'; +import { Dropdown } from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import SubTitle from '@/app/components/titles/SubTitle'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; +import MainTitle from '@/app/components/titles/MainTitle'; + +interface Report extends myMainCourseType { + name: string; + last_name: string; + father_name: string; + pending_count?: number | null; +} + +const TeacherCheckPage = () => { + const media = useMediaQuery('(max-width: 768px)'); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + const [report, setReport] = useState(null); + const [empty, setEmpty] = useState(false); + const [hasReport, setHasReport] = useState(false); + const [pagination, setPagination] = useState<{ currentPage: number; total: number; perPage: number }>({ + currentPage: 1, + total: 0, + perPage: 0 + }); + const [pageState, setPageState] = useState(1); + const [timeMode, setTimeMode] = useState<{ name_ru: string; code: number | null; id: number | null } | null>({ name_ru: '', code: null, id: null }); + const [timeModeOptions, setTimeModeOptions] = useState(null); + const [searchController, setSearchController] = useState(false); + + const [speciality, setSpecialyty] = useState<{ name_ru: string; code: number | null; id: number | null } | null>(null); + const [specialityOptions, setSpecialityOptions] = useState(null); + + const [currentFacultyId, setCurrentFacultyId] = useState(null); + const [currentSpecialityId, setCurrentSpecialityId] = useState(null); + const [progressSpinner, setProgressSpinner] = useState(false); + const [skeleton, setSkeleton] = useState(false); + const [search, setSearch] = useState(''); + + // Ручное управление пагинацией + const handlePageChange = (page: number) => { + setPageState(page); + handleAnswersReport(page, currentSpecialityId, search); + }; + + const handleFetchFaculty = async () => { + const data = await fetchFaculty(); + if (data && data?.length) { + const alls = { name_ru: translations.all, code: null, id: null }; + data.unshift(alls); + setTimeModeOptions(data); + } + }; + + const handleStudentSpeciality = async (id_faculty: number) => { + const data = await fetchSpeciality(id_faculty); + if (data && data?.length) { + const alls = { name_ru: translations.all, code: null, id: null }; + data.unshift(alls); + setSpecialityOptions(data); + // setStudents(data?.data); + } + }; + + // Вызываем отчет ответов + const handleAnswersReport = async (page: number, specialityId: number | null, search: string | null) => { + setSkeleton(true); + const data = await fetchAnwerReport(page, specialityId, search); + if (data?.success) { + setPagination({ + currentPage: data?.data?.current_page, + total: data?.data?.total, + perPage: data?.data?.per_page + }); + if (data?.data?.data?.length > 0) { + setEmpty(false); + setReport(data.data.data); + } else if (data?.data?.data?.length < 1) { + setEmpty(true); + setReport(null); + } + setHasReport(false); + } else { + setHasReport(true); + } + setSkeleton(false); + setProgressSpinner(false); + }; + + useEffect(() => { + setProgressSpinner(true); + if (search?.length === 0 && searchController) { + handleAnswersReport(pageState, currentSpecialityId, search); + setSearchController(false); + setProgressSpinner(false); + } + + if (search && search?.length < 2) { + setProgressSpinner(false); + return; + } + + setSearchController(true); + const delay = setTimeout(() => { + handleAnswersReport(pageState, currentSpecialityId, search); + setProgressSpinner(false); + }, 1000); + + return () => { + clearTimeout(delay); + }; + }, [search]); + + useEffect(() => { + if (timeMode?.id) { + setCurrentFacultyId(timeMode?.id); + } + }, [timeMode]); + + useEffect(() => { + if (currentFacultyId) { + handleStudentSpeciality(currentFacultyId); + } + }, [currentFacultyId]); + + useEffect(() => { + if (speciality) { + setCurrentSpecialityId(speciality?.id); + const specialityId = speciality?.id; + handleAnswersReport(pageState, specialityId, search); + } + }, [speciality]); + + useEffect(() => { + handleFetchFaculty(); + handleAnswersReport(pageState, currentSpecialityId, search); + }, []); + + // Update default values when language changes + useEffect(() => { + if (timeModeOptions && timeModeOptions.length > 0 && timeModeOptions[0].id === null) { + const updatedOptions = [...timeModeOptions]; + updatedOptions[0].name_ru = translations.all; + setTimeModeOptions(updatedOptions); + } + if (specialityOptions && specialityOptions.length > 0 && specialityOptions[0].id === null) { + const updatedOptions = [...specialityOptions]; + updatedOptions[0].name_ru = translations.all; + setSpecialityOptions(updatedOptions); + } + if (timeMode?.id === null) { + setTimeMode(prev => prev ? ({ ...prev, name_ru: translations.all }) : null); + } + if (speciality?.id === null) { + setSpecialyty(prev => prev ? ({ ...prev, name_ru: translations.all }) : null); + } + }, [translations]); + + const teacherNameBodyTemplate = (rowData: Report) => { + return ( +
+ {rowData?.last_name} + {rowData?.name} + {rowData?.father_name} +
+ ); + }; + + const uncheckedAssignmentsBodyTemplate = (rowData: Report) => { + return {rowData?.pending_count}; + }; + + const facultyItemTemplate = (option: any) => { + return {getLocalized(option, 'name') || option.name_ru}; + }; + + const facultyValueTemplate = (option: any) => { + if (!option) { + return {translations.selectFaculty}; + } + return {getLocalized(option, 'name') || option.name_ru}; + }; + + const specialityItemTemplate = (option: any) => { + return {getLocalized(option, 'name') || option.name_ru}; + }; + + const specialityValueTemplate = (option: any) => { + if (!option) { + return {translations.selectSpeciality}; + } + return {getLocalized(option, 'name') || option.name_ru}; + }; + + const DesktopTable = () => ( +
+ + rowIndex + 1} header="#" style={{ width: '20px' }}> + + + +
+ ); + + const MobileTable = () => ( +
+ {report?.length && + report.map((teacher) => ( +
+
+
+ { + + {teacher.name} {teacher.last_name} {teacher.father_name} + + } +
+
+ {translations.uncheckedAssignments}: + {uncheckedAssignmentsBodyTemplate(teacher)} +
+
+
+ ))} +
+ ); + + return ( +
+
+ {/* filter */} +
+ {translations.teacherReport} +
+
+ {translations.selectFaculty} +
+ setTimeMode(e.value)} + placeholder={translations.selectFaculty} + className="text-wrap word-break sm:text-nowrap sm:max-w-full" + itemTemplate={facultyItemTemplate} + valueTemplate={facultyValueTemplate} + /> +
+
+ +
+ {translations.selectSpeciality} +
+ setSpecialyty(e.value)} + placeholder={translations.selectSpeciality} + className={`${!specialityOptions ? 'pointer-events-none opacity-50' : ''} w-full text-sm`} + itemTemplate={specialityItemTemplate} + valueTemplate={specialityValueTemplate} + /> +
+
+
+
+ setSearch(e.target.value)} className="w-full p-inputtext-sm p-inputtext-rounded" /> +
{progressSpinner && }
+
+
+ + {hasReport ? ( +
+ +

{translations.loadingError}

+
+ ) : empty ? ( +
+ +

{translations.nothingFound}

+
+ ) : skeleton ? ( +
+ +
+ ) : media ? ( + + ) : ( + + )} + {!skeleton && ( + handlePageChange(e.page + 1)} + template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink" + /> + )} +
+
+ ); +}; + +export default TeacherCheckPage; diff --git a/app/(main)/students/[cource_id]/[connect_id]/[stream_id]/[student_id]/[from_student]/[lesson_id]/[step_id]/page.tsx b/app/(main)/students/[cource_id]/[connect_id]/[stream_id]/[student_id]/[from_student]/[lesson_id]/[step_id]/page.tsx new file mode 100644 index 00000000..6fca9c9c --- /dev/null +++ b/app/(main)/students/[cource_id]/[connect_id]/[stream_id]/[student_id]/[from_student]/[lesson_id]/[step_id]/page.tsx @@ -0,0 +1,296 @@ +'use client'; + +import ActivityPage from '@/app/components/Contribution'; +import LessonInfoCard from '@/app/components/lessons/LessonInfoCard'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { fetchCourseInfo } from '@/services/courses'; +import { statusView } from '@/services/notifications'; +import { fetchElement } from '@/services/steps'; +import { fetchStudentCalendar, fetchStudentDetail, pacticaDisannul, pacticaScoreAdd } from '@/services/streams'; +import { ContributionDay } from '@/types/ContributionDay'; +import { CourseType } from '@/types/courseType'; +import { lessonType } from '@/types/lessonType'; +import { mainStepsType } from '@/types/mainStepType'; +import { User } from '@/types/user'; +import { useParams, useSearchParams } from 'next/navigation'; +import { Accordion, AccordionTab } from 'primereact/accordion'; +import { useContext, useEffect, useState } from 'react'; + +export default function StudentCheck() { + const { cource_id, connect_id, stream_id, student_id, from_student, lesson_id, step_id } = useParams(); + + const { user, setMessage, contextNotificationId, setContextNotificationId, handleNotifications, contextFetchVerifed } = useContext(LayoutContext); + const { translations } = useLocalization(); + const showError = useErrorMessage(); + + const [mainSkeleton, mainSetSkeleton] = useState(false); + const [lessons, setLessons] = useState(null); + const [student, setStudent] = useState(null); + const [courseShow, setCourseShow] = useState(null); + const [hasSteps, setHasSteps] = useState(false); + const [element, setElement] = useState<{ content: any | null; step: mainStepsType } | null>(null); + const [contribution, setContribution] = useState(null); + const [skeleton, setSkeleton] = useState(false); + const [totalScore, setTotalScore] = useState(0); + + const [activeIndex, setActiveIndex] = useState(0); + + const handleCourseShow = async () => { + mainSetSkeleton(true); + const data = await fetchCourseInfo(cource_id ? Number(cource_id) : null); + if (data?.success) { + setCourseShow(data?.course); + } + mainSetSkeleton(false); + }; + + const handleFetchStreams = async () => { + mainSetSkeleton(true); + const data = await fetchStudentDetail(lesson_id ? Number(lesson_id) : null, connect_id ? Number(connect_id) : null, stream_id ? Number(stream_id) : null, student_id ? Number(student_id) : null, step_id ? Number(step_id) : null); + if (data?.success) { + // handleStatusView(); + setHasSteps(false); + mainSetSkeleton(false); + setLessons(data?.lessons); + setStudent(data?.student); + } else { + mainSetSkeleton(false); + setHasSteps(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const handleStatusView = async (notification_id: number | null) => { + if (notification_id) { + const data = await statusView(Number(notification_id)); + if (user?.is_working || user?.is_student) { + handleNotifications(); + } + setContextNotificationId(null); + } + }; + + const handleFetchElement = async (lesson_id: number, stepId: number) => { + if (lesson_id) { + setSkeleton(true); + const data = await fetchElement(Number(lesson_id), stepId); + if (data.success) { + setSkeleton(false); + setElement({ content: data.content, step: data.step }); + } else { + setSkeleton(false); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + } + }; + + const handleFetchCalendar = async () => { + mainSetSkeleton(true); + const data = await fetchStudentCalendar(connect_id ? Number(connect_id) : null, stream_id ? Number(stream_id) : null, student_id ? Number(student_id) : null); + + if (data && Array.isArray(data)) { + setContribution(data); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const handlePracticaScoreAdd = async (stepId: number, score: number) => { + const data = await pacticaScoreAdd(connect_id ? Number(connect_id) : null, stream_id ? Number(stream_id) : null, student_id ? Number(student_id) : null, stepId, score); + + if (data?.success) { + contextFetchVerifed(); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.successAdd, detail: '' } + }); + handleFetchStreams(); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.addError, detail: '' } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + const handlePracticaDisannul = async (id_curricula: number, course_id: number, id_stream: number, id: number, steps_id: number, message: string) => { + const data = await pacticaDisannul(id_curricula, course_id, id_stream, Number(student_id), steps_id, message); + + if (data?.success) { + contextFetchVerifed(); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.successAdd, detail: '' } + }); + handleFetchStreams(); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.addError, detail: '' } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + useEffect(() => { + handleCourseShow(); + handleFetchCalendar(); + handleFetchStreams(); + + if (contextNotificationId && contextNotificationId != null) { + handleStatusView(contextNotificationId); + } + }, []); + + useEffect(() => { + if (lessons && lessons?.length) { + lessons.forEach((item, idx) => { + if (item?.is_opened) { + setActiveIndex(idx); + return null; + } + }); + + // Вычисляем сумму баллов студента + let total = 0; + for (let i = 0; i < lessons.length; i++) { + for (let j = 0; j < lessons[i]?.steps?.length; j++) { + const step = lessons[i].steps[j]; + if (step.id_parent && step.ListAnswer) { + total += step.ListAnswer.score; + } + } + } + if (total) { + setTotalScore(total); + } + } + }, [lessons]); + + return ( +
+ {mainSkeleton ? ( +
+ +
+ ) : ( +
+ {courseShow && courseShow?.title ?

{courseShow?.title}

: ''} + +

+ {translations.activity}: + {student && ( +
+ {student?.last_name} + {student?.name && student?.name[0] + '.'} + {student?.father_name && student?.father_name[0] + '.'} +
+ )} +

+ +

{/* Название курса: {courseInfo.title} */}

+ setActiveIndex(e.index)}> + {lessons?.map((item) => { + const content = item?.steps?.filter((j) => { + return j?.id_parent != null; + }); + + return ( + +
+ {content?.length < 1 || hasSteps ? ( +

{translations.noData}

+ ) : ( + content?.map((i, idx) => { + if (i.id_parent) { + return ( +
+ { + {}} + + skeleton={skeleton} + + getValues={() => handleFetchElement(i?.lesson_id, i?.id)} + addPracticaScore={(score) => handlePracticaScoreAdd(i?.id, score)} + addPracticaDisannul={(id_curricula: number, course_id: number, id_stream: number, id: number, steps_id: number, message: string) => + handlePracticaDisannul(id_curricula, course_id, id_stream, id, steps_id, message) + } + + isOpened={i?.is_opened || false} + // item={i} + /> + } +
+ ); + } + }) + )} +
+
+ ); + })} +
+
+ {translations.yourScore}: + {totalScore} +
+
+ )} +
+ ); +} diff --git a/app/(main)/students/[cource_id]/[connect_id]/[stream_id]/page.tsx b/app/(main)/students/[cource_id]/[connect_id]/[stream_id]/page.tsx new file mode 100644 index 00000000..44cc933b --- /dev/null +++ b/app/(main)/students/[cource_id]/[connect_id]/[stream_id]/page.tsx @@ -0,0 +1,429 @@ +'use client'; + +import MyDateTime from '@/app/components/MyDateTime'; +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { fetchScoreValues, fetchStreams, fetchStreamStudents, sendMyeduScore } from '@/services/streams'; +import { mainStreamsType } from '@/types/mainStreamsType'; +import { OptionsType } from '@/types/OptionsType'; +import Link from 'next/link'; +import { useParams } from 'next/navigation'; +import { Button } from 'primereact/button'; +import { Column } from 'primereact/column'; +import { DataTable } from 'primereact/datatable'; +import { Dialog } from 'primereact/dialog'; +import React, { useContext, useEffect, useState } from 'react'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; + +export default function StudentList() { + // types + + interface ScoreValueType { + course: { id: number; title: string }; + teacher: { last_name: string; name: string }; + id_stream: number; + score: number | null; + schedule: { active: boolean; expired: boolean; from: string; to: string }; + } + + const { cource_id, connect_id, stream_id } = useParams(); + const media = useMediaQuery('(max-width: 640px)'); + + const [studentList, setStudentList] = useState([]); + const [hasList, setHasList] = useState(false); + const [skeleton, setSkeleton] = useState(false); + const [streams, setStreams] = useState([]); + const [stream, setStream] = useState(null); + const [myEduInfoVisible, setMyEduInfoVisible] = useState(false); + const [hasScoreValue, setHasScoreValue] = useState(false); + const [scoreValues, setScoreValues] = useState([]); + const [studentId, setStudentId] = useState(null); + const [studentScore, setStudentScore] = useState(null); + const [exportBtnSkeleton, setExportBtnSkeleton] = useState(false); + const { setMessage } = useContext(LayoutContext); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + const showError = useErrorMessage(); + + const options: OptionsType = { + year: '2-digit', + month: '2-digit', // 'long', 'short', 'numeric' + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false // 24‚ + }; + + // functions + const handleFetchStreams = async () => { + if (cource_id) { + const data = await fetchStreams(cource_id ? Number(cource_id) : null, 25); + if (data) { + setStreams(data); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + } + }; + + const handleFetchStudents = async () => { + const data = await fetchStreamStudents(connect_id ? Number(connect_id) : null, stream_id ? Number(stream_id) : null); + setSkeleton(true); + if (data) { + setHasList(false); + setStudentList(data); + } else { + setHasList(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + setSkeleton(false); + }; + + const handleFetchScoreValues = async (id_extra_type: number | null,stream_id: number, student_id: number | null, score: number) => { + setStudentId(student_id); + setStudentScore(score); + setMyEduInfoVisible(true); + setExportBtnSkeleton(true); + const checkingExtraType = id_extra_type == null ? 0 : 1; + console.log(id_extra_type); + console.log(checkingExtraType); + + const data = await fetchScoreValues(checkingExtraType ,stream_id, student_id); + if (data) { + const scoresArr: ScoreValueType[] = Object.values(data); + if (scoresArr && scoresArr?.length > 0) { + setScoreValues(scoresArr); + setHasScoreValue(false); + } else if (scoresArr?.length < 1) { + setHasScoreValue(true); + } + } else { + setHasScoreValue(true); + if (data?.response?.status == '400') { + const teachers = () => { + if (data?.response?.data?.teacher) { + return ( +
+
+ + {data?.response?.data?.teacher?.last_name} {data?.response?.data?.teacher?.name && data?.response?.data?.teacher?.name[0] + '.'}{' '} + {data?.response?.data?.teacher?.father_name && data?.response?.data?.teacher?.father_name.length > 1 && data?.response?.data?.teacher?.father_name[0] + '.'} + +
+
+ ); + } else { + return ''; + } + }; + setMessage({ + state: true, + value: { + severity: 'error', + summary: data?.response?.data?.message, + detail:
{teachers()}
+ } + }); + } + } + setExportBtnSkeleton(false); + }; + + const handleSendMyeduScore = async (stream_id: number, student_id: number | null, score: number | null) => { + setMyEduInfoVisible(false); + setSkeleton(true); + const data = await sendMyeduScore(stream_id, student_id, score); + + if (data.success) { + handleFetchStudents(); + const scoresArr: ScoreValueType[] = Object.values(data); + if (scoresArr && scoresArr?.length > 0) { + setScoreValues(scoresArr); + setHasScoreValue(false); + } else if (scoresArr?.length < 1) { + setHasScoreValue(true); + } + setMessage({ + state: true, + value: { severity: 'success', summary: translations.sendSuccess, detail: '' } + }); + } else { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { + severity: 'error', + summary: translations.errorTitle, + detail: data?.response?.data?.message + } + }); + } else { + showError(data.response.status); + } + } + setStudentId(null); + setStudentScore(null); + setSkeleton(false); + }; + + const item = scoreValues?.[0]; // берем первый элемент безопасно + const isExportDisabled = !item || !item.schedule?.active || !!item.schedule?.expired; + + const footerContent = ( +
+
+ ); + + useEffect(() => { + handleFetchStreams(); + handleFetchStudents(); + }, []); + + useEffect(() => { + if (streams && streams?.length > 0 && stream_id) { + const forStream = streams.find((item) => item.stream_id === Number(stream_id)); + if (forStream) { + setStream(forStream); + } + } + }, [streams]); + + return ( +
+ {skeleton ? ( + + ) : ( + <> + {/* info section */} +
+

+ {getLocalized(stream?.subject_name, 'name') || stream?.subject_name.name_ru} +

+ +
+ {getLocalized(stream?.semester, 'name') || stream?.semester?.name_ru} + + {stream?.id_extra_type === null ? translations.auditItem : stream?.id_extra_type != null ? translations.notAuditItem : ''} + +
+ {getLocalized(stream?.subject_type_name, 'name') || stream?.subject_type_name?.name_ru} + {/* {stream?.teacher?.name} */} +
+
+ {translations.languageOfStudy}: + {stream?.language?.name} +
+
+ {translations.studyYear}: + 20{stream?.id_edu_year} +
+
+ {translations.period}: + {getLocalized(stream?.period, 'name') || stream?.period.name_ru} +
+
+ {getLocalized(stream?.edu_form, 'name') || stream?.edu_form?.name_ru} +
+
+
+ + )} + + {/* table section */} + {hasList ? ( + + ) : ( +
+ {skeleton ? ( + + ) : ( + <> + + rowIndex + 1} header={translations.numberSign} style={{ width: '20px' }}> + ( +
+ {rowData?.last_name} + {rowData?.name} + {rowData?.father_name} +
+ )} + >
+ + { + return ( +
+ {rowData?.score && rowData.score > 0 ? ( +
+ 30 ? 'text-[var(--greenColor)] p-1 w-[25px] text-center' : 'text-amber-400 p-1 w-[25px] text-center '}`}>{rowData.score} + {!rowData?.export ? ( + handleFetchScoreValues(stream?.id_extra_type || null, Number(stream_id), rowData?.id || null, rowData?.score)} + className="cursor-pointer pi pi-upload bg-[var(--mainColor)] text-white p-2 px-3 rounded" + title={translations.saveToMyedu} + > + ) : ( + '' + )} +
+ ) : ( + {rowData?.score} + )} +
+ ); + }} + /> + + ( +
+ {/* {rowData?.last_movement ? new Date(rowData.last_movement).toISOString().slice(0, 19).replace('T', ' ') : } */} + {rowData?.last_movement ? : } +
+ )} + /> + + ( +
+ {rowData?.last_movement && ( + +
+ )} + /> +
+ + )} +
+ )} + + {/* dialog */} + { + if (!myEduInfoVisible) return; + setMyEduInfoVisible(false); + setStudentId(null); + setStudentScore(null); + }} + footer={footerContent} + > + <> + {exportBtnSkeleton ? ( +
+ +
+ ) : hasScoreValue ? ( +
+ {translations.dataNotAvailable} +
+ ) : ( +
+ {scoreValues?.map((item: ScoreValueType) => { + return ( +
1 && 'p-2 lesson-card-border shadow'}`}> +
+ {item?.course?.title} +
+ {translations.idLabel}: {item?.id_stream} +
+
+
+

+ {item?.teacher?.last_name} {item?.teacher?.name} +

+
+ {translations.score}: {item?.score || 0} +
+
+ {item?.schedule && ( +
+

{translations.periodExportShedule}

+
+ + - + +
+
+ )} + +
+

{translations.status}

+ {item?.schedule?.active ? ( +
+ + {/*{translations.accessExportStatus}*/} +
+ ) : ( +
+ + {/*{translations.accessExportStatusFalse}*/} +
+ )} +
+
+ ); + })} +
+ )} + +
+
+ ); +} diff --git a/app/(main)/students/forum/[stepId]/[id_parent]/[forum_id]/page.tsx b/app/(main)/students/forum/[stepId]/[id_parent]/[forum_id]/page.tsx new file mode 100644 index 00000000..935dd811 --- /dev/null +++ b/app/(main)/students/forum/[stepId]/[id_parent]/[forum_id]/page.tsx @@ -0,0 +1,561 @@ +'use client'; + +import { NotFound } from '@/app/components/NotFound'; +import FormModal from '@/app/components/popUp/FormModal'; +import Redacting from '@/app/components/popUp/Redacting'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { addForumMessage, deleteMessageForum, forumDetails, forumDetailsShow, updateMessageForum } from '@/services/forum'; +import { getConfirmOptions } from '@/utils/getConfirmOptions'; +import { getRedactor } from '@/utils/getRedactor'; +import Link from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { Button } from 'primereact/button'; +import { confirmDialog } from 'primereact/confirmdialog'; +import { InputText } from 'primereact/inputtext'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function Forum() { + type OptionsType = Intl.DateTimeFormatOptions; + + // data + interface answerType { + created_at: string; + description: string; + forum_id: number; + id: number; + parent_id: number | null; + steps_id: number; + updated_at: string; + user: { id: number; name: string }; + user_id: number; + } + + // main + interface mainAnswerType { + current_page: number; + data: answerType[]; + first_page_url: string; + from: number; + last_page: number; + last_page_url: string; + links: [{ url: string | null; label: string; active: boolean }]; + next_page_url: null; + path: string; + per_page: number; + prev_page_url: null; + to: number; + total: number; + } + + const { stepId, id_parent, forum_id } = useParams(); + const {translations} = useLocalization(); + const { user, setMessage, forumValuse } = useContext(LayoutContext); + const showError = useErrorMessage(); + const media = useMediaQuery('(max-width: 640px)'); + const scrollRef = useRef(); + const messagesEndRef = useRef(null); + + const [selectId, setSelectId] = useState(null); + const [visible, setVisisble] = useState(false); + const [isLoadingOlder, setIsLoadingOlder] = useState(false); + const [forumValue, setForumValue] = useState({ + current_page: 0, + data: [], + first_page_url: '', + from: 0, + last_page: 0, + last_page_url: '', + links: [{ url: '', label: '', active: false }], + next_page_url: null, + path: '', + per_page: 0, + prev_page_url: null, + to: 0, + total: 0 + }); + + const [sendMessage, setSendMessage] = useState(''); + const [progressSpinner, setProgressSpinner] = useState(false); + const [bigProgressSpinner, setBigProgressSpinner] = useState(false); + const [sendBtnDisabled, setSendBtnDisabled] = useState(false); + const [currentPage, setCurrentPage] = useState(0); + const [editingLesson, setEditingLesson] = useState<{ description: string } | null>(null); + const [descCommentState, setDescCommentState] = useState(false); + const [answerToComment, setAnswerToComment] = useState<{ userInfo: { userName: string }; description: string; id: number } | null>(null); + const [hasComments, setHasComments] = useState(false); + const [valueEmpty, setValueEmpty] = useState(false); + const [skeleton, setSkeleton] = useState(true); + const [forumInfoValues, setInfoForumValues] = useState<{ description: string; userInfo: { userName: string; userLastName: string } } | null>(null); + + const router = useRouter(); + + // 2. Функция, выполняющая прокрутку + const scrollToTop = () => { + if (messagesEndRef.current) { + // Устанавливаем scrollTop в 0 для прокрутки в самый верх + messagesEndRef.current.scrollTop = 0; + } + }; + + const dateTime = (createdAt: string | null) => { + const invalidDate = ---; + if (createdAt) { + const dateObject = new Date(createdAt); + if (dateObject) { + const options: OptionsType = { + month: 'short', // 'long', 'short', 'numeric' + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false // 24-часовой формат + }; + const formattedString = dateObject.toLocaleString('ru-RU', options); + const result = formattedString?.replace(/,/g, ''); + if (formattedString) { + return {result}; + } else { + return invalidDate; + } + } else { + return invalidDate; + } + } else { + return invalidDate; + } + }; + + const handleForumDetails = async () => { + if (skeleton) { + setTimeout(() => { + setSkeleton(false); + }, 1000); + } + + setIsLoadingOlder(true); // сразу же отключаем запрос + setProgressSpinner(true); + const data = await forumDetails(Number(forum_id), currentPage); + if (data && Object.values(data).length > 0) { + setHasComments(false); + setProgressSpinner(false); + + if (data?.data?.length < 1) { + // если очередной или первый запрос возвращает пустой массив то отключам состояние и прекращаем на этом делать новые запросы + setIsLoadingOlder(true); + } else { + setIsLoadingOlder(false); // если же данные пришли то значит и дальше есть скорее есть данные значит включаем состояние + } + + const forData = [...(forumValue?.data ?? []), ...(data?.data || [])]; + const newStructure: mainAnswerType = { + current_page: data?.current_page, + first_page_url: '', + data: forData, + from: data?.from, + last_page: data?.last_page, + last_page_url: data?.last_page_url, + links: [{ url: '', label: '', active: false }], + next_page_url: data?.next_page_url, + path: data?.path, + per_page: data?.per_page, + prev_page_url: data?.prev_page_url, + to: data?.to, + total: data?.total + }; + setForumValue(newStructure); + } else { + setHasComments(true); + setIsLoadingOlder(true); // останавливаем если ошибка + setProgressSpinner(false); + } + }; + + const editing = async (id: number) => { + const data = await forumDetailsShow(id); + + if (data && Object.values(data).length) { + setEditingLesson({ description: data?.description }); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const selectedForEditing = (id: number) => { + setSelectId(id); + setVisisble(true); + editing(id); + }; + + // const handleForumMore = () => { + // }; + + // ДОБАВЛЕНИЕ СООБЩЕНИЙ + const handleAddMessage = async () => { + setIsLoadingOlder(true); + setProgressSpinner(true); + const data = await addForumMessage(Number(stepId), answerToComment?.id || null, sendMessage); + console.log(data); + + if (data.success) { + setSendMessage(''); + setIsLoadingOlder(false); + setProgressSpinner(false); + setForumValue((prev) => (prev ? { ...prev, data: [data?.data, ...prev.data] } : prev)); + // setForumValue(data?.data); + setMessage({ + state: true, + value: { severity: 'success', summary: translations.successAdd, detail: '' } + }); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.addError, detail: '' } + }); + if (data.response.status) { + showError(data.response.status); + } + } + }; + + const handleDelete = async (id: number) => { + setBigProgressSpinner(true); + + const data = await deleteMessageForum(id); + console.log(data); + + if (data.success) { + setBigProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'success', summary: 'Успешно удалено!', detail: '' } + }); + setForumValue((prev) => + prev + ? { + ...prev, + data: prev.data.filter((item) => { + return item?.id !== id; + }) + } + : prev + ); + } else { + setBigProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при удалении!', detail: '' } + }); + if (data.response.status) { + showError(data.response.status); + } + } + }; + + // update document + const handleUpdate = async () => { + setBigProgressSpinner(true); + const data = await updateMessageForum(selectId, (editingLesson && editingLesson?.description) || ''); + console.log(data); + + if (data?.success) { + setBigProgressSpinner(false); + setForumValue((prev) => + prev + ? { + ...prev, + data: prev.data.map((item) => + item.id === data.data.id + ? { ...item, description: data.data.description } // обновляем нужное поле + : item + ) + } + : prev + ); + + setMessage({ + state: true, + value: { severity: 'success', summary: 'Успешно изменено!', detail: '' } + }); + } else { + setBigProgressSpinner(false); + setEditingLesson({ description: '' }); + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка при изменении!', detail: '' } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const handleScroll = (e: { currentTarget: { scrollTop: number; clientHeight: number; scrollHeight: number } }) => { + const { scrollTop, clientHeight, scrollHeight } = e.currentTarget; + + // Рассчитываем, насколько близко мы к низу. + // Мы на самом низу, когда: scrollTop + clientHeight === scrollHeight + // Устанавливаем "зазор" (например, 100px), чтобы начать загрузку заранее: + const isNearBottom = scrollTop + clientHeight >= scrollHeight - 250; + + if (isNearBottom && !isLoadingOlder) { + // Вызов функции для загрузки БОЛЕЕ НОВЫХ сообщений + // handleAddMessage(); + handleForumDetails(); + } + }; + + const chatSection = (item: answerType, messageAnswer: boolean) => { + const parendItem = item?.parent_id ? forumValue.data.find((el) => el.id === item.parent_id) : false; + return ( +
+ {parendItem && ( + { + e.preventDefault(); + if(item?.parent_id){ + const target = document.getElementById(item.parent_id + ''); + if (target) { + target.scrollIntoView({ behavior: 'smooth' }); // плавный скролл + } + } + }} + > + +
+ {item?.parent_id ? parendItem?.user.name : ''} + {item?.parent_id ? parendItem?.description : ''} +
+
+ )} +
+
+ {item?.user?.name} + +
+ {user?.id === item?.user?.id && {})} textSize={'14px'} />} +
+
+
+
+
{item?.description}
+
+
+ {messageAnswer && ( +
+ + { + setDescCommentState(true); + setAnswerToComment({ userInfo: { userName: item?.user?.name }, description: item?.description, id: item?.id }); + scrollToTop(); + }} + > + Ответить + +
+ )} +

{dateTime(item?.updated_at)}

+
+
+
+
+ ); + }; + + // Вызываем детаилс + useEffect(() => { + // const testFunc = () => { + // const clearValue = setInterval(() => { + handleForumDetails(); + // }, 3000); + // if(hasComments || ha){ + // clearInterval(clearValue); + // } + // } + // testFunc(); + const checkValues = localStorage.getItem('forumValues'); + if (checkValues && checkValues?.length > 0) { + setInfoForumValues(JSON.parse(checkValues)); + } + }, []); + + useEffect(() => { + console.log(forumValue); + if (forumValue?.current_page) { + setCurrentPage(forumValue.current_page); + } + + if (forumValue?.data?.length < 1) { + setValueEmpty(true); + } else { + setValueEmpty(false); + } + }, [forumValue]); + + return ( +
+
+ {/* header section */} +
+
+ +
+ {/*
+
+

Название курса {!media && '-'}

+

Название темы

+
+ xx-xx-xx +
*/} +
+
+ {forumInfoValues?.description ?

Название форума: {forumInfoValues?.description}

:

Форум

} + {forumInfoValues?.userInfo?.userName && ( + + {forumInfoValues?.userInfo?.userLastName} {forumInfoValues?.userInfo?.userName} + + )} +
+ {/* xx-xx-xx */} +
+
+ + {skeleton ? ( +
+ +
+ ) : hasComments ? ( + + ) : ( + <> + {/* chat */} +
+ {bigProgressSpinner && ( +
+ +
+ )} +
+ {valueEmpty ? ( +
+

Сообщения отсутствуют. Добавьте первое, чтобы начать диалог

+
+ ) : ( + //
+ //
+ //
+ //
+ // setDescCommentState(false)}> + //

Ответить пользователю ({''})

+ //
+ //

{dateTime('')}

+ //
+ //
{'lorem'}
+ //
+ + // {Array.isArray(forumValue?.data) && + // forumValue?.data?.map((item) => { + // return ( + //
+ // {chatSection(item, false)} + //
+ // ); + // })} + //
+
+ {descCommentState && ( +
+
+

Ответить пользователю ({answerToComment?.userInfo?.userName})

+ setDescCommentState(false)}> +
+ +
{answerToComment?.description}
+
+ )} + + {Array.isArray(forumValue?.data) && + forumValue?.data?.map((item) => { + return
{chatSection(item, true)}
; + })} +
+ )} + + {progressSpinner && } +
+
+ {/* send area */} +
+ { + if (e.key === 'Enter') { + handleAddMessage(); + setSendBtnDisabled(true); + setTimeout(() => { + setSendBtnDisabled(false); + }, 1000); + } + }} + placeholder={descCommentState ? 'Введите свой ответ' : ''} + value={sendMessage} + onChange={(e) => setSendMessage((prev) => (prev = e.target.value))} + /> + +
+ + )} +
+ + handleUpdate()} clearValues={() => {}} visible={visible} setVisible={setVisisble} start={false} footerValue={{ footerState: true, reject: 'Закрыть', next: 'Сохранить' }}> +
+
+ { + setEditingLesson( + (prev) => + prev && { + ...prev, + description: e.target.value + } + ); + }} + /> + {progressSpinner && } +
+
+
+
+ ); +} diff --git a/app/(main)/students/search/page.tsx b/app/(main)/students/search/page.tsx new file mode 100644 index 00000000..a8e1e056 --- /dev/null +++ b/app/(main)/students/search/page.tsx @@ -0,0 +1,164 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import { InputText } from 'primereact/inputtext'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { fetchStudentsForTeacher } from '@/services/student/studentSearch'; +import SubTitle from '@/app/components/titles/SubTitle'; +import Link from 'next/link'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import MainTitle from '@/app/components/titles/MainTitle'; + +type Student = { + id: number; + name: string; + last_name: string; + father_name: string; + email: string; + myedu_id: string; + status: 'active' | 'inactive' | 'suspended'; + registrationDate: string; +}; + +const StudentSearchPage = () => { + const { translations } = useLocalization(); + + const [searchTerm, setSearchTerm] = useState(''); + const [debouncedTerm, setDebouncedTerm] = useState(''); + const [students, setStudents] = useState([]); + const [studentEmpty, setStudentsEmpty] = useState(false); + const [startDisplay, setStartDisplay] = useState(true); + const [mainSpinner, setMainSpinner] = useState(false); + const [progressSpinner, setProgressSpinner] = useState(false); + const isMobile = useMediaQuery('(max-width: 640px)'); + + const handleStudentSearch = async (term: string) => { + setProgressSpinner(true); + setMainSpinner(true); + const data = await fetchStudentsForTeacher(term); + + if (data && data?.success) { + setStartDisplay(false); + if (data?.data.length > 0) { + setStudents(data?.data); + setStudentsEmpty(false); + } else { + setStudentsEmpty(true); + } + } else { + setStartDisplay(true); + } + setProgressSpinner(false); + setMainSpinner(false); + }; + + // Debounce effect for search input + useEffect(() => { + const timerId = setTimeout(() => { + setDebouncedTerm(searchTerm); + }, 1000); + + return () => { + clearTimeout(timerId); + }; + }, [searchTerm]); + + useEffect(() => { + if (debouncedTerm) { + handleStudentSearch(debouncedTerm); + } + }, [debouncedTerm]); + + const searchSection = ( +
+ {translations.students} + +
+ setSearchTerm(e.target.value)} className="w-full p-inputtext-sm p-inputtext-rounded" /> +
{progressSpinner && }
+
+
+
+ ); + + const studentsDesktop = ( +
+ + rowIndex + 1} header="#" style={{ width: '20px' }} /> + {translations.fullName}} + sortable + body={(rowData) => { + return ( + + {rowData?.last_name} + {rowData?.name} + {rowData?.father_name} + + ); + }} + style={{ minWidth: '14rem' }} + /> + Email} sortable style={{ minWidth: '14rem' }} /> + {translations.personalShortNumber}} + body={(rowData) => {rowData?.myedu_id}} + sortable + sortField="status" + style={{ minWidth: '8rem' }} + /> + +
+ ); + + const studentsMobile = ( +
+ {students.map((student) => ( +
+
+ + {student.last_name} {student.name} {student.father_name} + +
+ Email: {student.email} +
+
+ Личный номер: {student.myedu_id} +
+
+
+ ))} +
+ ); + + return ( +
+ {searchSection} + + {startDisplay ? ( +
+
+ +
+
+ ) : mainSpinner ? ( +
+ +
+ ) : studentEmpty ? ( + Студенты не найдены + ) : isMobile ? ( + studentsMobile + ) : ( + studentsDesktop + )} +
+ ); +}; + +export default StudentSearchPage; diff --git a/app/(main)/students/search/studentDetail/[id_student]/page.tsx b/app/(main)/students/search/studentDetail/[id_student]/page.tsx new file mode 100644 index 00000000..8ef10fb9 --- /dev/null +++ b/app/(main)/students/search/studentDetail/[id_student]/page.tsx @@ -0,0 +1,183 @@ +'use client'; + +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { fetchStudentDetail } from '@/services/streams'; +import { fetchStudentSearchDetail, fetchStudentSearchImg } from '@/services/student/studentSearch'; +import { RoleUserType } from '@/types/roles/RoleUserType'; +import { useParams } from 'next/navigation'; +import React, { useState, useEffect } from 'react'; + +interface StudentDetail extends RoleUserType { + birth_date: string; +} + +interface StudentInfo { + title: string; + id: number; + modules: {score: number, id_stream: number}[]; +} + +const StudentDetailPage = () => { + const { translations } = useLocalization(); + + const { id_student } = useParams(); + const media = useMediaQuery('(max-width: 640px)'); + + const [skeleton, setSkeleton] = useState(true); + const [studentDetail, setStudentDetail] = useState(null); + const [profileImg, setImg] = useState<{ image_url: string; id: number } | null>(null); + const [courses, setCourses] = useState([]); + + const handleFetchStudentDetail = async () => { + // setSkeleton(true); + const data = await fetchStudentSearchDetail(Number(id_student)); + if (data?.success) { + setStudentDetail(data?.student); + setCourses(data?.data); + } else { + } + setSkeleton(false); + }; + + const handleFetchStudentImg = async () => { + const data = await fetchStudentSearchImg(Number(id_student)); + if (data?.success) { + setImg({ image_url: data?.data?.image_url, id: data?.data?.id }); + } + }; + + const coursesTable = ( +
+ + + + + + + + + + + {courses.map((course) => ( + + + + + + + ))} + +
+ {translations.courseName} + + Модуль + + Балл + + ID +
+
{course?.title}
+
+
+ {course?.modules?.length ? ( +
+ + {translations.passed} +
+ ) : ( +
+ + {translations.failed} +
+ )} +
+
+
{course?.modules[0]?.score}
+
+
{course?.modules[0]?.id_stream}
+
+
+ ); + + const coursesMobile = ( +
+ {courses.map((course) => ( +
+
{course?.title}
+
+ Балл: + {course?.modules[0]?.score} +
+
+ Модуль: +
+ {course?.modules?.length ? ( +
+ + {translations.passed} +
+ ) : ( +
+ + {translations.failed} +
+ )} +
+
+
ID {course?.modules[0]?.id_stream}
+
+ ))} +
+ ); + + useEffect(() => { + handleFetchStudentDetail(); + handleFetchStudentImg(); + }, []); + + if (skeleton) { + return ( +
+ + +
+ ); + } + + return ( +
+
+ {/* Student Info Card */} +
+
+ Фото студента +
+
+

+ {studentDetail?.last_name} {studentDetail?.name} {studentDetail?.father_name} +

+

{studentDetail?.email}

+
+

+ {translations.personalNumber}: {studentDetail?.myedu_id} +

+

+ {translations.yearBirdth}: {studentDetail?.birth_date || '-'} +

+
+
+
+ + {/* Courses Section */} +
+

{translations.courses}

+ {media ? coursesMobile : coursesTable} +
+
+
+ ); +}; + +export default StudentDetailPage; diff --git a/app/(main)/uikit/button/index.module.scss b/app/(main)/uikit/button/index.module.scss deleted file mode 100644 index 2ee14829..00000000 --- a/app/(main)/uikit/button/index.module.scss +++ /dev/null @@ -1,151 +0,0 @@ -.p-button{ - padding: 0%; -&.google { - background: linear-gradient(to left, var(--purple-600) 50%, var(--purple-700) 50%); - background-size: 200% 100%; - background-position: right bottom; - transition: background-position 0.5s ease-out; - border-color: var(--purple-700); - display: flex; - align-items: stretch; - padding: 0; - - - &:enabled:hover { - background: linear-gradient(to left, var(--purple-600) 50%, var(--purple-700) 50%); - background-size: 200% 100%; - background-position: left bottom; - border-color: var(--purple-700); - } - - &:focus { - box-shadow: 0 0 0 1px var(--purple-400); - } -} - -&.twitter { - background: linear-gradient(to left, var(--blue-400) 50%, var(--blue-500) 50%); - background-size: 200% 100%; - background-position: right bottom; - transition: background-position 0.5s ease-out; - border-color: var(--blue-500); - padding: 0; - display: flex; - align-items: stretch; - - &:enabled:hover { - background: linear-gradient(to left, var(--blue-400) 50%, var(--blue-500) 50%); - background-size: 200% 100%; - background-position: left bottom; - border-color: var(--blue-500); - } - - &:focus { - box-shadow: 0 0 0 1px var(--blue-200); - } -} - -&.discord { - background: linear-gradient(to left, var(--bluegray-700) 50%, var(--bluegray-800) 50%); - background-size: 200% 100%; - background-position: right bottom; - transition: background-position 0.5s ease-out; - border-color: var(--bluegray-800); - padding: 0; - display: flex; - align-items: stretch; - - &:enabled:hover { - background: linear-gradient(to left, var(--bluegray-700) 50%, var(--bluegray-800) 50%); - background-size: 200% 100%; - background-position: left bottom; - border-color: var(--bluegray-800); - } - - &:focus { - box-shadow: 0 0 0 1px var(--purple-500); - } -} - -.template-button .p-button.twitter { - background: linear-gradient(to left, var(--blue-400) 50%, var(--blue-500) 50%); - background-size: 200% 100%; - background-position: right bottom; - transition: background-position 0.5s ease-out; - color: #fff; - border-color: var(--blue-500); -} -.template-button .p-button.twitter:hover { - background-position: left bottom; -} -.template-button .p-button.twitter i { - background-color: var(--blue-500); -} -.template-button .p-button.twitter:focus { - box-shadow: 0 0 0 1px var(--blue-200); -} -.template-button .p-button.slack { - background: linear-gradient(to left, var(--orange-400) 50%, var(--orange-500) 50%); - background-size: 200% 100%; - background-position: right bottom; - transition: background-position 0.5s ease-out; - color: #fff; - border-color: var(--orange-500); -} -.template-button .p-button.slack:hover { - background-position: left bottom; -} -.template-button .p-button.slack i { - background-color: var(--orange-500); -} -.template-button .p-button.slack:focus { - box-shadow: 0 0 0 1px var(--orange-200); -} -.template-button .p-button.amazon { - background: linear-gradient(to left, var(--yellow-400) 50%, var(--yellow-500) 50%); - background-size: 200% 100%; - background-position: right bottom; - transition: background-position 0.5s ease-out; - color: #000; - border-color: var(--yellow-500); -} -.template-button .p-button.amazon:hover { - background-position: left bottom; -} -.template-button .p-button.amazon i { - background-color: var(--yellow-500); -} -.template-button .p-button.amazon:focus { - box-shadow: 0 0 0 1px var(--yellow-200); -} -.template-button .p-button.discord { - background: linear-gradient(to left, var(--bluegray-700) 50%, var(--bluegray-800) 50%); - background-size: 200% 100%; - background-position: right bottom; - transition: background-position 0.5s ease-out; - color: #fff; - border-color: var(--bluegray-800); -} -.template-button .p-button.discord:hover { - background-position: left bottom; -} -.template-button .p-button.discord i { - background-color: var(--bluegray-800); -} -.template-button .p-button.discord:focus { - box-shadow: 0 0 0 1px var(--bluegray-500); -} -@media screen and (max-width: 960px) { - -button .p-button { - margin-bottom: 0.5rem; - } - -button .p-button:not(.p-button-icon-only) { - display: flex; - flex-wrap: wrap; - - } - -button .p-buttonset .p-button { - margin-bottom: 0; - } -} -} \ No newline at end of file diff --git a/app/(main)/uikit/button/page.tsx b/app/(main)/uikit/button/page.tsx deleted file mode 100644 index 69196ed2..00000000 --- a/app/(main)/uikit/button/page.tsx +++ /dev/null @@ -1,248 +0,0 @@ -'use client'; -import React, { useState } from 'react'; -import { SplitButton } from 'primereact/splitbutton'; -import { Button } from 'primereact/button'; -import styles from './index.module.scss'; -import { classNames } from 'primereact/utils'; - -const ButtonDemo = () => { - const [loading1, setLoading1] = useState(false); - const [loading2, setLoading2] = useState(false); - const [loading3, setLoading3] = useState(false); - const [loading4, setLoading4] = useState(false); - - const onLoadingClick1 = () => { - setLoading1(true); - - setTimeout(() => { - setLoading1(false); - }, 2000); - }; - - const onLoadingClick2 = () => { - setLoading2(true); - - setTimeout(() => { - setLoading2(false); - }, 2000); - }; - - const onLoadingClick3 = () => { - setLoading3(true); - - setTimeout(() => { - setLoading3(false); - }, 2000); - }; - - const onLoadingClick4 = () => { - setLoading4(true); - - setTimeout(() => { - setLoading4(false); - }, 2000); - }; - - const items = [ - { - label: 'Update', - icon: 'pi pi-refresh' - }, - { - label: 'Delete', - icon: 'pi pi-times' - }, - { - label: 'Home', - icon: 'pi pi-home' - } - ]; - - return ( -
-
-
-
Default
-
- - - -
-
- -
-
Severities
-
-
-
- -
-
Text
-
-
-
- -
-
Outlined
-
-
-
- -
-
Button Group
- -
- -
-
SplitButton
-
- - - - - -
-
- -
-
Template
-
- - - -
-
-
- -
-
-
Icons
-
- - - -
-
- -
-
Raised
-
-
-
- -
-
Rounded
-
-
-
- -
-
Rounded Icons
-
-
-
- -
-
Rounded Text
-
-
-
- -
-
Rounded Outlined
-
-
-
- -
-
Loading
-
-
-
-
-
- ); -}; - -export default ButtonDemo; diff --git a/app/(main)/uikit/charts/page.tsx b/app/(main)/uikit/charts/page.tsx deleted file mode 100644 index 7ccc85af..00000000 --- a/app/(main)/uikit/charts/page.tsx +++ /dev/null @@ -1,283 +0,0 @@ -'use client'; -import { ChartData, ChartOptions } from 'chart.js'; -import { Chart } from 'primereact/chart'; -import React, { useContext, useEffect, useState } from 'react'; -import { LayoutContext } from '../../../../layout/context/layoutcontext'; -import type { ChartDataState, ChartOptionsState } from '@/types'; - -const ChartDemo = () => { - const [options, setOptions] = useState({}); - const [data, setChartData] = useState({}); - const { layoutConfig } = useContext(LayoutContext); - - useEffect(() => { - const documentStyle = getComputedStyle(document.documentElement); - const textColor = documentStyle.getPropertyValue('--text-color') || '#495057'; - const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary') || '#6c757d'; - const surfaceBorder = documentStyle.getPropertyValue('--surface-border') || '#dfe7ef'; - const barData: ChartData = { - labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], - datasets: [ - { - label: 'My First dataset', - backgroundColor: documentStyle.getPropertyValue('--primary-500') || '#6366f1', - borderColor: documentStyle.getPropertyValue('--primary-500') || '#6366f1', - data: [65, 59, 80, 81, 56, 55, 40] - }, - { - label: 'My Second dataset', - backgroundColor: documentStyle.getPropertyValue('--primary-200') || '#bcbdf9', - borderColor: documentStyle.getPropertyValue('--primary-200') || '#bcbdf9', - data: [28, 48, 40, 19, 86, 27, 90] - } - ] - }; - - const barOptions: ChartOptions = { - plugins: { - legend: { - labels: { - color: textColor - } - } - }, - scales: { - x: { - ticks: { - color: textColorSecondary, - font: { - weight: '500' - } - }, - grid: { - display: false - }, - border: { - display: false - } - }, - y: { - ticks: { - color: textColorSecondary - }, - grid: { - color: surfaceBorder - }, - border: { - display: false - } - } - } - }; - - const pieData: ChartData = { - labels: ['A', 'B', 'C'], - datasets: [ - { - data: [540, 325, 702], - backgroundColor: [documentStyle.getPropertyValue('--indigo-500') || '#6366f1', documentStyle.getPropertyValue('--purple-500') || '#a855f7', documentStyle.getPropertyValue('--teal-500') || '#14b8a6'], - hoverBackgroundColor: [documentStyle.getPropertyValue('--indigo-400') || '#8183f4', documentStyle.getPropertyValue('--purple-400') || '#b975f9', documentStyle.getPropertyValue('--teal-400') || '#41c5b7'] - } - ] - }; - - const pieOptions: ChartOptions = { - plugins: { - legend: { - labels: { - usePointStyle: true, - color: textColor - } - } - } - }; - - const lineData: ChartData = { - labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], - datasets: [ - { - label: 'First Dataset', - data: [65, 59, 80, 81, 56, 55, 40], - fill: false, - backgroundColor: documentStyle.getPropertyValue('--primary-500') || '#6366f1', - borderColor: documentStyle.getPropertyValue('--primary-500') || '#6366f1', - tension: 0.4 - }, - { - label: 'Second Dataset', - data: [28, 48, 40, 19, 86, 27, 90], - fill: false, - backgroundColor: documentStyle.getPropertyValue('--primary-200') || '#bcbdf9', - borderColor: documentStyle.getPropertyValue('--primary-200') || '#bcbdf9', - tension: 0.4 - } - ] - }; - - const lineOptions: ChartOptions = { - plugins: { - legend: { - labels: { - color: textColor - } - } - }, - scales: { - x: { - ticks: { - color: textColorSecondary - }, - grid: { - color: surfaceBorder - }, - border: { - display: false - } - }, - y: { - ticks: { - color: textColorSecondary - }, - grid: { - color: surfaceBorder - }, - border: { - display: false - } - } - } - }; - - const polarData: ChartData = { - datasets: [ - { - data: [11, 16, 7, 3], - backgroundColor: [ - documentStyle.getPropertyValue('--indigo-500') || '#6366f1', - documentStyle.getPropertyValue('--purple-500') || '#a855f7', - documentStyle.getPropertyValue('--teal-500') || '#14b8a6', - documentStyle.getPropertyValue('--orange-500') || '#f97316' - ], - label: 'My dataset' - } - ], - labels: ['Indigo', 'Purple', 'Teal', 'Orange'] - }; - - const polarOptions: ChartOptions = { - plugins: { - legend: { - labels: { - color: textColor - } - } - }, - scales: { - r: { - grid: { - color: surfaceBorder - } - } - } - }; - - const radarData: ChartData = { - labels: ['Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running'], - datasets: [ - { - label: 'My First dataset', - borderColor: documentStyle.getPropertyValue('--indigo-400') || '#8183f4', - pointBackgroundColor: documentStyle.getPropertyValue('--indigo-400') || '#8183f4', - pointBorderColor: documentStyle.getPropertyValue('--indigo-400') || '#8183f4', - pointHoverBackgroundColor: textColor, - pointHoverBorderColor: documentStyle.getPropertyValue('--indigo-400') || '#8183f4', - data: [65, 59, 90, 81, 56, 55, 40] - }, - { - label: 'My Second dataset', - borderColor: documentStyle.getPropertyValue('--purple-400') || '#b975f9', - pointBackgroundColor: documentStyle.getPropertyValue('--purple-400') || '#b975f9', - pointBorderColor: documentStyle.getPropertyValue('--purple-400') || '#b975f9', - pointHoverBackgroundColor: textColor, - pointHoverBorderColor: documentStyle.getPropertyValue('--purple-400') || '#b975f9', - data: [28, 48, 40, 19, 96, 27, 100] - } - ] - }; - - const radarOptions: ChartOptions = { - plugins: { - legend: { - labels: { - color: textColor - } - } - }, - scales: { - r: { - grid: { - color: textColorSecondary - } - } - } - }; - - setOptions({ - barOptions, - pieOptions, - lineOptions, - polarOptions, - radarOptions - }); - setChartData({ - barData, - pieData, - lineData, - polarData, - radarData - }); - }, [layoutConfig]); - - return ( -
-
-
-
Linear Chart
- -
-
-
-
-
Bar Chart
- -
-
-
-
-
Pie Chart
- -
-
-
-
-
Doughnut Chart
- -
-
-
-
-
Polar Area Chart
- -
-
-
-
-
Radar Chart
- -
-
-
- ); -}; - -export default ChartDemo; diff --git a/app/(main)/uikit/file/page.tsx b/app/(main)/uikit/file/page.tsx deleted file mode 100644 index 9dd41f9b..00000000 --- a/app/(main)/uikit/file/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; - -import React, { useRef } from 'react'; -import { FileUpload } from 'primereact/fileupload'; -import { Toast } from 'primereact/toast'; - -const FileDemo = () => { - const toast = useRef(null); - - const onUpload = () => { - toast.current?.show({ - severity: 'info', - summary: 'Success', - detail: 'File Uploaded', - life: 3000 - }); - }; - - return ( -
- -
-
-
Advanced
- - -
Basic
- -
-
-
- ); -}; - -export default FileDemo; diff --git a/app/(main)/uikit/floatlabel/page.tsx b/app/(main)/uikit/floatlabel/page.tsx deleted file mode 100644 index 6d196fe6..00000000 --- a/app/(main)/uikit/floatlabel/page.tsx +++ /dev/null @@ -1,215 +0,0 @@ -"use client"; -import type { Demo } from "@/types"; -import { - AutoComplete, - AutoCompleteCompleteEvent, -} from "primereact/autocomplete"; -import { Calendar } from "primereact/calendar"; -import { Chips } from "primereact/chips"; -import { Dropdown } from "primereact/dropdown"; -import { InputMask } from "primereact/inputmask"; -import { InputNumber } from "primereact/inputnumber"; -import { InputText } from "primereact/inputtext"; -import { InputTextarea } from "primereact/inputtextarea"; -import { MultiSelect } from "primereact/multiselect"; -import { useEffect, useState } from "react"; -import { CountryService } from "../../../../demo/service/CountryService"; - -const FloatLabelDemo = () => { - const [countries, setCountries] = useState([]); - const [filteredCountries, setFilteredCountries] = useState( - [] - ); - const [value1, setValue1] = useState(""); - const [value2, setValue2] = useState(null); - const [value3, setValue3] = useState(""); - const [value4, setValue4] = useState(""); - const [value5, setValue5] = useState(null); - const [value6, setValue6] = useState([]); - const [value7, setValue7] = useState(""); - const [value8, setValue8] = useState(null); - const [value9, setValue9] = useState(""); - const [value10, setValue10] = useState(null); - const [value11, setValue11] = useState(null); - const [value12, setValue12] = useState(""); - - const cities = [ - { name: "New York", code: "NY" }, - { name: "Rome", code: "RM" }, - { name: "London", code: "LDN" }, - { name: "Istanbul", code: "IST" }, - { name: "Paris", code: "PRS" }, - ]; - - useEffect(() => { - CountryService.getCountries().then((countries) => { - setCountries(countries); - }); - }, []); - - const searchCountry = (event: AutoCompleteCompleteEvent) => { - const filtered = []; - const query = event.query; - for (let i = 0; i < countries.length; i++) { - const country = countries[i]; - if (country.name.toLowerCase().indexOf(query.toLowerCase()) === 0) { - filtered.push(country); - } - } - setFilteredCountries(filtered); - }; - - return ( -
-
Float Label
-

- All input text components support floating labels by adding ( - .p-float-label) to wrapper class. -

-
-
- - setValue1(e.target.value)} - /> - - -
-
- - setValue2(e.value)} - suggestions={filteredCountries} - completeMethod={searchCountry} - field="name" - > - - -
-
- - - setValue3(e.target.value)} - /> - - -
-
- - - setValue4(e.target.value)} - /> - - -
-
- - setValue5(e.value ?? "")} - > - - -
-
- - setValue6(e.value ?? [])} - > - - -
-
- - setValue7(e.value ?? "")} - > - - -
-
- - - setValue8(e.target.value ?? null) - } - > - - -
-
-
- - - - - setValue9(e.target.value)} - /> - - -
-
-
- - setValue10(e.value)} - optionLabel="name" - > - - -
-
- - setValue11(e.value)} - optionLabel="name" - > - - -
-
- - setValue12(e.target.value)} - > - - -
-
-
- ); -}; - -export default FloatLabelDemo; diff --git a/app/(main)/uikit/formlayout/page.tsx b/app/(main)/uikit/formlayout/page.tsx deleted file mode 100644 index 01e98052..00000000 --- a/app/(main)/uikit/formlayout/page.tsx +++ /dev/null @@ -1,148 +0,0 @@ -'use client'; - -import React, { useState, useEffect, useMemo } from 'react'; -import { InputText } from 'primereact/inputtext'; -import { Button } from 'primereact/button'; -import { InputTextarea } from 'primereact/inputtextarea'; -import { Dropdown } from 'primereact/dropdown'; - -interface DropdownItem { - name: string; - code: string; -} - -const FormLayoutDemo = () => { - const [dropdownItem, setDropdownItem] = useState(null); - const dropdownItems: DropdownItem[] = useMemo( - () => [ - { name: 'Option 1', code: 'Option 1' }, - { name: 'Option 2', code: 'Option 2' }, - { name: 'Option 3', code: 'Option 3' } - ], - [] - ); - - useEffect(() => { - setDropdownItem(dropdownItems[0]); - }, [dropdownItems]); - - return ( -
-
-
-
Vertical
-
- - -
-
- - -
-
- - -
-
- -
-
Vertical Grid
-
-
- - -
-
- - -
-
-
-
- -
-
-
Horizontal
-
- -
- -
-
-
- -
- -
-
-
- -
-
Inline
-
-
- - -
-
- - -
- -
-
- -
-
Help Text
-
- - - Enter your username to reset your password. -
-
-
- -
-
-
Advanced
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - setDropdownItem(e.value)} options={dropdownItems} optionLabel="name" placeholder="Select One"> -
-
- - -
-
-
-
-
- ); -}; - -export default FormLayoutDemo; diff --git a/app/(main)/uikit/input/page.tsx b/app/(main)/uikit/input/page.tsx deleted file mode 100644 index 27fcffb4..00000000 --- a/app/(main)/uikit/input/page.tsx +++ /dev/null @@ -1,522 +0,0 @@ -"use client"; -import type { Demo, Page } from "@/types"; -import { - AutoComplete, - AutoCompleteCompleteEvent, -} from "primereact/autocomplete"; -import { Button } from "primereact/button"; -import { Calendar } from "primereact/calendar"; -import { Checkbox, CheckboxChangeEvent } from "primereact/checkbox"; -import { Chips } from "primereact/chips"; -import { - ColorPicker, - ColorPickerHSBType, - ColorPickerRGBType, -} from "primereact/colorpicker"; -import { Dropdown } from "primereact/dropdown"; -import { InputNumber } from "primereact/inputnumber"; -import { InputSwitch } from "primereact/inputswitch"; -import { InputText } from "primereact/inputtext"; -import { InputTextarea } from "primereact/inputtextarea"; -import { Knob } from "primereact/knob"; -import { ListBox } from "primereact/listbox"; -import { MultiSelect } from "primereact/multiselect"; -import { RadioButton } from "primereact/radiobutton"; -import { Rating } from "primereact/rating"; -import { SelectButton } from "primereact/selectbutton"; -import { Slider } from "primereact/slider"; -import { ToggleButton } from "primereact/togglebutton"; -import { useEffect, useState } from "react"; -import { CountryService } from "../../../../demo/service/CountryService"; - -interface InputValue { - name: string; - code: string; -} - -const InputDemo: Page = () => { - const [floatValue, setFloatValue] = useState(""); - const [autoValue, setAutoValue] = useState([]); - const [selectedAutoValue, setSelectedAutoValue] = useState(null); - const [autoFilteredValue, setAutoFilteredValue] = useState( - [] - ); - const [calendarValue, setCalendarValue] = useState(null); - const [inputNumberValue, setInputNumberValue] = useState( - null - ); - const [chipsValue, setChipsValue] = useState([]); - const [sliderValue, setSliderValue] = useState(""); - const [ratingValue, setRatingValue] = useState(null); - const [colorValue, setColorValue] = useState< - string | ColorPickerRGBType | ColorPickerHSBType - >("1976D2"); - const [knobValue, setKnobValue] = useState(20); - const [radioValue, setRadioValue] = useState(null); - const [checkboxValue, setCheckboxValue] = useState([]); - const [switchValue, setSwitchValue] = useState(false); - const [listboxValue, setListboxValue] = useState(null); - const [dropdownValue, setDropdownValue] = useState(null); - const [multiselectValue, setMultiselectValue] = useState(null); - const [toggleValue, setToggleValue] = useState(false); - const [selectButtonValue1, setSelectButtonValue1] = useState(null); - const [selectButtonValue2, setSelectButtonValue2] = useState(null); - const [inputGroupValue, setInputGroupValue] = useState(false); - - const listboxValues: InputValue[] = [ - { name: "New York", code: "NY" }, - { name: "Rome", code: "RM" }, - { name: "London", code: "LDN" }, - { name: "Istanbul", code: "IST" }, - { name: "Paris", code: "PRS" }, - ]; - - const dropdownValues: InputValue[] = [ - { name: "New York", code: "NY" }, - { name: "Rome", code: "RM" }, - { name: "London", code: "LDN" }, - { name: "Istanbul", code: "IST" }, - { name: "Paris", code: "PRS" }, - ]; - - const multiselectValues: InputValue[] = [ - { name: "Australia", code: "AU" }, - { name: "Brazil", code: "BR" }, - { name: "China", code: "CN" }, - { name: "Egypt", code: "EG" }, - { name: "France", code: "FR" }, - { name: "Germany", code: "DE" }, - { name: "India", code: "IN" }, - { name: "Japan", code: "JP" }, - { name: "Spain", code: "ES" }, - { name: "United States", code: "US" }, - ]; - - const selectButtonValues1: InputValue[] = [ - { name: "Option 1", code: "O1" }, - { name: "Option 2", code: "O2" }, - { name: "Option 3", code: "O3" }, - ]; - - const selectButtonValues2: InputValue[] = [ - { name: "Option 1", code: "O1" }, - { name: "Option 2", code: "O2" }, - { name: "Option 3", code: "O3" }, - ]; - - useEffect(() => { - CountryService.getCountries().then((data) => setAutoValue(data)); - }, []); - - const searchCountry = (event: AutoCompleteCompleteEvent) => { - setTimeout(() => { - if (!event.query.trim().length) { - setAutoFilteredValue([...autoValue]); - } else { - setAutoFilteredValue( - autoValue.filter((country) => { - return country.name - .toLowerCase() - .startsWith(event.query.toLowerCase()); - }) - ); - } - }, 250); - }; - - const onCheckboxChange = (e: CheckboxChangeEvent) => { - let selectedValue = [...checkboxValue]; - if (e.checked) selectedValue.push(e.value); - else selectedValue.splice(selectedValue.indexOf(e.value), 1); - - setCheckboxValue(selectedValue); - }; - - const itemTemplate = (option: InputValue) => { - return ( -
- {option.name} - (e.currentTarget.src = - "https://www.primefaces.org/wp-content/uploads/2020/05/placeholder.png") - } - className={`flag flag-${option.code.toLowerCase()}`} - style={{ width: "21px" }} - /> - {option.name} -
- ); - }; - - return ( -
-
-
-
InputText
-
-
- -
-
- -
-
- -
-
- -
Icons
-
-
- - - - -
-
- - - - -
-
- - - - - -
-
- -
Float Label
- - setFloatValue(e.target.value)} - /> - - - -
Textarea
- - -
AutoComplete
- setSelectedAutoValue(e.value)} - suggestions={autoFilteredValue} - completeMethod={searchCountry} - field="name" - /> - -
Calendar
- setCalendarValue(e.value ?? null)} - /> - -
InputNumber
- - setInputNumberValue(e.value ?? null) - } - showButtons - mode="decimal" - > - -
Chips
- setChipsValue(e.value ?? [])} - /> -
- -
-
-
-
Slider
- - setSliderValue(parseInt(e.target.value, 10)) - } - /> - - setSliderValue(e.value as number) - } - /> -
-
-
Rating
- setRatingValue(e.value ?? 0)} - /> -
-
-
ColorPicker
- setColorValue(e.value ?? "")} - style={{ width: "2rem" }} - /> -
-
-
Knob
- setKnobValue(e.value)} - step={10} - min={-50} - max={50} - /> -
-
-
-
- -
-
-
RadioButton
-
-
-
- setRadioValue(e.value)} - /> - -
-
-
-
- setRadioValue(e.value)} - /> - -
-
-
-
- setRadioValue(e.value)} - /> - -
-
-
- -
Checkbox
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- -
Input Switch
- setSwitchValue(e.value ?? false)} - /> -
- -
-
Listbox
- setListboxValue(e.value)} - options={listboxValues} - optionLabel="name" - filter - /> - -
Dropdown
- setDropdownValue(e.value)} - options={dropdownValues} - optionLabel="name" - placeholder="Select" - /> - -
MultiSelect
- setMultiselectValue(e.value)} - options={multiselectValues} - itemTemplate={itemTemplate} - optionLabel="name" - placeholder="Select Countries" - filter - className="multiselect-custom" - display="chip" - /> -
- -
-
ToggleButton
- setToggleValue(e.value)} - onLabel="Yes" - offLabel="No" - /> - -
SelectButton
- setSelectButtonValue1(e.value)} - options={selectButtonValues1} - optionLabel="name" - /> - -
SelectButton - Multiple
- setSelectButtonValue2(e.value)} - options={selectButtonValues2} - optionLabel="name" - multiple - /> -
-
- -
-
-
Input Groups
-
-
-
- - - - -
-
- -
-
- - - - - - - - $ - .00 -
-
- -
-
-
-
- -
-
- - - setInputGroupValue( - e.checked ?? false - ) - } - /> - - -
-
-
-
-
-
- ); -}; - -export default InputDemo; diff --git a/app/(main)/uikit/invalidstate/page.tsx b/app/(main)/uikit/invalidstate/page.tsx deleted file mode 100644 index 3dece247..00000000 --- a/app/(main)/uikit/invalidstate/page.tsx +++ /dev/null @@ -1,186 +0,0 @@ -"use client"; - -import type { Demo } from "@/types"; -import { - AutoComplete, - AutoCompleteCompleteEvent, -} from "primereact/autocomplete"; -import { Calendar } from "primereact/calendar"; -import { Chips } from "primereact/chips"; -import { Dropdown } from "primereact/dropdown"; -import { InputMask } from "primereact/inputmask"; -import { InputNumber } from "primereact/inputnumber"; -import { InputText } from "primereact/inputtext"; -import { InputTextarea } from "primereact/inputtextarea"; -import { MultiSelect } from "primereact/multiselect"; -import { Password } from "primereact/password"; -import { useEffect, useState } from "react"; -import { CountryService } from "../../../../demo/service/CountryService"; - -const InvalidStateDemo = () => { - const [countries, setCountries] = useState([]); - const [filteredCountries, setFilteredCountries] = useState( - [] - ); - const [value1, setValue1] = useState(""); - const [value2, setValue2] = useState(null); - const [value3, setValue3] = useState(null); - const [value4, setValue4] = useState([]); - const [value5, setValue5] = useState(""); - const [value6, setValue6] = useState(""); - const [value7, setValue7] = useState(0); - const [value8, setValue8] = useState(null); - const [value9, setValue9] = useState(null); - const [value10, setValue10] = useState(""); - - const cities = [ - { name: "New York", code: "NY" }, - { name: "Rome", code: "RM" }, - { name: "London", code: "LDN" }, - { name: "Istanbul", code: "IST" }, - { name: "Paris", code: "PRS" }, - ]; - - useEffect(() => { - CountryService.getCountries().then((countries) => { - setCountries(countries); - }); - }, []); - - const searchCountry = (event: AutoCompleteCompleteEvent) => { - // in a real application, make a request to a remote url with the query and - // return filtered results, for demo we filter at client side - const filtered = []; - const query = event.query; - for (let i = 0; i < countries.length; i++) { - const country = countries[i]; - if (country.name.toLowerCase().indexOf(query.toLowerCase()) === 0) { - filtered.push(country); - } - } - setFilteredCountries(filtered); - }; - - const onCalendarChange = (e: any) => { - setValue3(e.value!); - }; - - return ( -
-
Invalid State
-
-
-
- - setValue1(e.target.value)} - className="p-invalid" - /> -
-
- - setValue2(e.value)} - suggestions={filteredCountries} - completeMethod={searchCountry} - field="name" - className="p-invalid" - /> -
-
- - -
-
- - setValue4(e.value ?? [])} - className="p-invalid" - /> -
-
- - setValue5(e.target.value)} - className="p-invalid" - /> -
-
- -
-
- - setValue6(e.value ?? "")} - className="p-invalid" - /> -
-
- - - setValue7(e.target.value ?? 0) - } - className="p-invalid" - /> -
-
- - setValue8(e.value)} - optionLabel="name" - className="p-invalid" - /> -
-
- - setValue9(e.value)} - optionLabel="name" - className="p-invalid" - /> -
-
- - setValue10(e.target.value)} - className="p-invalid" - /> -
-
-
-
- ); -}; - -export default InvalidStateDemo; diff --git a/app/(main)/uikit/list/page.tsx b/app/(main)/uikit/list/page.tsx deleted file mode 100644 index 31e24b53..00000000 --- a/app/(main)/uikit/list/page.tsx +++ /dev/null @@ -1,192 +0,0 @@ -'use client'; - -import React, { useState, useEffect } from 'react'; -import { DataView, DataViewLayoutOptions } from 'primereact/dataview'; -import { Button } from 'primereact/button'; -import { Dropdown, DropdownChangeEvent } from 'primereact/dropdown'; -import { Rating } from 'primereact/rating'; -import { PickList } from 'primereact/picklist'; -import { OrderList } from 'primereact/orderlist'; -import { ProductService } from '../../../../demo/service/ProductService'; -import { InputText } from 'primereact/inputtext'; -import type { Demo } from '@/types'; - -const ListDemo = () => { - const listValue = [ - { name: 'San Francisco', code: 'SF' }, - { name: 'London', code: 'LDN' }, - { name: 'Paris', code: 'PRS' }, - { name: 'Istanbul', code: 'IST' }, - { name: 'Berlin', code: 'BRL' }, - { name: 'Barcelona', code: 'BRC' }, - { name: 'Rome', code: 'RM' } - ]; - - const [picklistSourceValue, setPicklistSourceValue] = useState(listValue); - const [picklistTargetValue, setPicklistTargetValue] = useState([]); - const [orderlistValue, setOrderlistValue] = useState(listValue); - const [dataViewValue, setDataViewValue] = useState([]); - const [globalFilterValue, setGlobalFilterValue] = useState(''); - const [filteredValue, setFilteredValue] = useState(null); - const [layout, setLayout] = useState<'grid' | 'list' | (string & Record)>('grid'); - const [sortKey, setSortKey] = useState(null); - const [sortOrder, setSortOrder] = useState<0 | 1 | -1 | null>(null); - const [sortField, setSortField] = useState(''); - - const sortOptions = [ - { label: 'Price High to Low', value: '!price' }, - { label: 'Price Low to High', value: 'price' } - ]; - - useEffect(() => { - ProductService.getProducts().then((data) => setDataViewValue(data)); - setGlobalFilterValue(''); - }, []); - - useEffect(() => { - ProductService.getProducts().then((data) => setDataViewValue(data)); - setGlobalFilterValue(''); - }, []); - - const onFilter = (e: React.ChangeEvent) => { - const value = e.target.value; - setGlobalFilterValue(value); - if (value.length === 0) { - setFilteredValue(null); - } else { - const filtered = dataViewValue?.filter((product) => { - const productNameLowercase = product.name.toLowerCase(); - const searchValueLowercase = value.toLowerCase(); - return productNameLowercase.includes(searchValueLowercase); - }); - - setFilteredValue(filtered); - } - }; - - const onSortChange = (event: DropdownChangeEvent) => { - const value = event.value; - - if (value.indexOf('!') === 0) { - setSortOrder(-1); - setSortField(value.substring(1, value.length)); - setSortKey(value); - } else { - setSortOrder(1); - setSortField(value); - setSortKey(value); - } - }; - - const dataViewHeader = ( -
- - - - - - setLayout(e.value)} /> -
- ); - - const dataviewListItem = (data: Demo.Product) => { - return ( -
-
- {data.name} -
-
{data.name}
-
{data.description}
- -
- - {data.category} -
-
-
- ${data.price} - - {data.inventoryStatus} -
-
-
- ); - }; - - const dataviewGridItem = (data: Demo.Product) => { - return ( -
-
-
-
- - {data.category} -
- {data.inventoryStatus} -
-
- {data.name} -
{data.name}
-
{data.description}
- -
-
- ${data.price} -
-
-
- ); - }; - - const itemTemplate = (data: Demo.Product, layout: 'grid' | 'list' | (string & Record)) => { - if (!data) { - return; - } - - if (layout === 'list') { - return dataviewListItem(data); - } else if (layout === 'grid') { - return dataviewGridItem(data); - } - }; - - return ( -
-
-
-
DataView
- -
-
- -
-
-
PickList
-
{item.name}
} - onChange={(e) => { - setPicklistSourceValue(e.source); - setPicklistTargetValue(e.target); - }} - sourceStyle={{ height: '200px' }} - targetStyle={{ height: '200px' }} - >
-
-
- -
-
-
OrderList
-
{item.name}
} onChange={(e) => setOrderlistValue(e.value)}>
-
-
-
- ); -}; - -export default ListDemo; diff --git a/app/(main)/uikit/media/page.tsx b/app/(main)/uikit/media/page.tsx deleted file mode 100644 index 269f5da1..00000000 --- a/app/(main)/uikit/media/page.tsx +++ /dev/null @@ -1,109 +0,0 @@ -'use client'; - -import { Button } from 'primereact/button'; -import { Carousel } from 'primereact/carousel'; -import { Galleria } from 'primereact/galleria'; -import { Image } from 'primereact/image'; -import React, { useEffect, useState } from 'react'; -import { PhotoService } from '../../../../demo/service/PhotoService'; -import { ProductService } from '../../../../demo/service/ProductService'; -import type { Demo } from '@/types'; - -const MediaDemo = () => { - const [products, setProducts] = useState([]); - const [images, setImages] = useState([]); - - const galleriaResponsiveOptions = [ - { - breakpoint: '1024px', - numVisible: 5 - }, - { - breakpoint: '960px', - numVisible: 4 - }, - { - breakpoint: '768px', - numVisible: 3 - }, - { - breakpoint: '560px', - numVisible: 1 - } - ]; - const carouselResponsiveOptions = [ - { - breakpoint: '1024px', - numVisible: 3, - numScroll: 3 - }, - { - breakpoint: '768px', - numVisible: 2, - numScroll: 2 - }, - { - breakpoint: '560px', - numVisible: 1, - numScroll: 1 - } - ]; - - useEffect(() => { - ProductService.getProductsSmall().then((products) => setProducts(products)); - - PhotoService.getImages().then((images) => setImages(images)); - }, []); - - const carouselItemTemplate = (product: Demo.Product) => { - return ( -
-
- {product.name} -
-
-

{product.name}

-
${product.price}
- {product.inventoryStatus} -
- - - -
-
-
- ); - }; - - const galleriaItemTemplate = (item: Demo.Photo) => {item.alt}; - const galleriaThumbnailTemplate = (item: Demo.Photo) => {item.alt}; - - return ( -
-
-
-
Carousel
- -
-
- -
-
-
Image
-
- Image -
-
-
- -
-
-
Galleria
- -
-
-
- ); -}; - -export default MediaDemo; diff --git a/app/(main)/uikit/menu/confirmation/page.tsx b/app/(main)/uikit/menu/confirmation/page.tsx deleted file mode 100644 index 8b5b8d2f..00000000 --- a/app/(main)/uikit/menu/confirmation/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client'; - -import React from 'react'; -import Menu from '../page'; -function ConfirmationDemo() { - return ( - -
- -

Confirmation Component Content via Child Route

-
-
- ); -} - -export default ConfirmationDemo; diff --git a/app/(main)/uikit/menu/page.tsx b/app/(main)/uikit/menu/page.tsx deleted file mode 100644 index c49836bd..00000000 --- a/app/(main)/uikit/menu/page.tsx +++ /dev/null @@ -1,584 +0,0 @@ -'use client'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Menubar } from 'primereact/menubar'; -import { InputText } from 'primereact/inputtext'; -import { BreadCrumb } from 'primereact/breadcrumb'; -import { Steps } from 'primereact/steps'; -import { TabMenu } from 'primereact/tabmenu'; -import { TieredMenu } from 'primereact/tieredmenu'; -import { Menu } from 'primereact/menu'; -import { Button } from 'primereact/button'; -import { ContextMenu } from 'primereact/contextmenu'; -import { MegaMenu } from 'primereact/megamenu'; -import { PanelMenu } from 'primereact/panelmenu'; -import { useRouter } from 'next/navigation'; -import { usePathname } from 'next/navigation'; - -const MenuDemo = ({ children }: any) => { - const [activeIndex, setActiveIndex] = useState(0); - const menu = useRef(null); - const contextMenu = useRef(null); - const router = useRouter(); - const pathname = usePathname(); - - const checkActiveIndex = useCallback(() => { - const paths = pathname.split('/'); - const currentPath = paths[paths.length - 1]; - - switch (currentPath) { - case 'seat': - setActiveIndex(1); - break; - case 'payment': - setActiveIndex(2); - break; - case 'confirmation': - setActiveIndex(3); - break; - default: - break; - } - }, [pathname]); - - useEffect(() => { - checkActiveIndex(); - }, [checkActiveIndex]); - - const nestedMenuitems = [ - { - label: 'Customers', - icon: 'pi pi-fw pi-table', - items: [ - { - label: 'New', - icon: 'pi pi-fw pi-user-plus', - items: [ - { - label: 'Customer', - icon: 'pi pi-fw pi-plus' - }, - { - label: 'Duplicate', - icon: 'pi pi-fw pi-copy' - } - ] - }, - { - label: 'Edit', - icon: 'pi pi-fw pi-user-edit' - } - ] - }, - { - label: 'Orders', - icon: 'pi pi-fw pi-shopping-cart', - items: [ - { - label: 'View', - icon: 'pi pi-fw pi-list' - }, - { - label: 'Search', - icon: 'pi pi-fw pi-search' - } - ] - }, - { - label: 'Shipments', - icon: 'pi pi-fw pi-envelope', - items: [ - { - label: 'Tracker', - icon: 'pi pi-fw pi-compass' - }, - { - label: 'Map', - icon: 'pi pi-fw pi-map-marker' - }, - { - label: 'Manage', - icon: 'pi pi-fw pi-pencil' - } - ] - }, - { - label: 'Profile', - icon: 'pi pi-fw pi-user', - items: [ - { - label: 'Settings', - icon: 'pi pi-fw pi-cog' - }, - { - label: 'Billing', - icon: 'pi pi-fw pi-file' - } - ] - }, - { - label: 'Quit', - icon: 'pi pi-fw pi-sign-out' - } - ]; - - const breadcrumbHome = { icon: 'pi pi-home', to: '/' }; - const breadcrumbItems = [{ label: 'Computer' }, { label: 'Notebook' }, { label: 'Accessories' }, { label: 'Backpacks' }, { label: 'Item' }]; - - const wizardItems = [ - { label: 'Personal', command: () => router.push('/uikit/menu') }, - { label: 'Seat', command: () => router.push('/uikit/menu/seat') }, - { label: 'Payment', command: () => router.push('/uikit/menu/payment') }, - { - label: 'Confirmation', - command: () => router.push('/uikit/menu/confirmation') - } - ]; - - const tieredMenuItems = [ - { - label: 'Customers', - icon: 'pi pi-fw pi-table', - items: [ - { - label: 'New', - icon: 'pi pi-fw pi-user-plus', - items: [ - { - label: 'Customer', - icon: 'pi pi-fw pi-plus' - }, - { - label: 'Duplicate', - icon: 'pi pi-fw pi-copy' - } - ] - }, - { - label: 'Edit', - icon: 'pi pi-fw pi-user-edit' - } - ] - }, - { - label: 'Orders', - icon: 'pi pi-fw pi-shopping-cart', - items: [ - { - label: 'View', - icon: 'pi pi-fw pi-list' - }, - { - label: 'Search', - icon: 'pi pi-fw pi-search' - } - ] - }, - { - label: 'Shipments', - icon: 'pi pi-fw pi-envelope', - items: [ - { - label: 'Tracker', - icon: 'pi pi-fw pi-compass' - }, - { - label: 'Map', - icon: 'pi pi-fw pi-map-marker' - }, - { - label: 'Manage', - icon: 'pi pi-fw pi-pencil' - } - ] - }, - { - label: 'Profile', - icon: 'pi pi-fw pi-user', - items: [ - { - label: 'Settings', - icon: 'pi pi-fw pi-cog' - }, - { - label: 'Billing', - icon: 'pi pi-fw pi-file' - } - ] - }, - { - separator: true - }, - { - label: 'Quit', - icon: 'pi pi-fw pi-sign-out' - } - ]; - - const overlayMenuItems = [ - { - label: 'Save', - icon: 'pi pi-save' - }, - { - label: 'Update', - icon: 'pi pi-refresh' - }, - { - label: 'Delete', - icon: 'pi pi-trash' - }, - { - separator: true - }, - { - label: 'Home', - icon: 'pi pi-home' - } - ]; - - const menuitems = [ - { - label: 'Customers', - items: [ - { - label: 'New', - icon: 'pi pi-fw pi-plus' - }, - { - label: 'Edit', - icon: 'pi pi-fw pi-user-edit' - } - ] - }, - { - label: 'Orders', - items: [ - { - label: 'View', - icon: 'pi pi-fw pi-list' - }, - { - label: 'Search', - icon: 'pi pi-fw pi-search' - } - ] - } - ]; - - const contextMenuItems = [ - { - label: 'Save', - icon: 'pi pi-save' - }, - { - label: 'Update', - icon: 'pi pi-refresh' - }, - { - label: 'Delete', - icon: 'pi pi-trash' - }, - { - separator: true - }, - { - label: 'Options', - icon: 'pi pi-cog' - } - ]; - - const megamenuItems = [ - { - label: 'Fashion', - icon: 'pi pi-fw pi-tag', - items: [ - [ - { - label: 'Woman', - items: [{ label: 'Woman Item' }, { label: 'Woman Item' }, { label: 'Woman Item' }] - }, - { - label: 'Men', - items: [{ label: 'Men Item' }, { label: 'Men Item' }, { label: 'Men Item' }] - } - ], - [ - { - label: 'Kids', - items: [{ label: 'Kids Item' }, { label: 'Kids Item' }] - }, - { - label: 'Luggage', - items: [{ label: 'Luggage Item' }, { label: 'Luggage Item' }, { label: 'Luggage Item' }] - } - ] - ] - }, - { - label: 'Electronics', - icon: 'pi pi-fw pi-desktop', - items: [ - [ - { - label: 'Computer', - items: [{ label: 'Computer Item' }, { label: 'Computer Item' }] - }, - { - label: 'Camcorder', - items: [{ label: 'Camcorder Item' }, { label: 'Camcorder Item' }, { label: 'Camcorder Item' }] - } - ], - [ - { - label: 'TV', - items: [{ label: 'TV Item' }, { label: 'TV Item' }] - }, - { - label: 'Audio', - items: [{ label: 'Audio Item' }, { label: 'Audio Item' }, { label: 'Audio Item' }] - } - ], - [ - { - label: 'Sports.7', - items: [{ label: 'Sports.7.1' }, { label: 'Sports.7.2' }] - } - ] - ] - }, - { - label: 'Furniture', - icon: 'pi pi-fw pi-image', - items: [ - [ - { - label: 'Living Room', - items: [{ label: 'Living Room Item' }, { label: 'Living Room Item' }] - }, - { - label: 'Kitchen', - items: [{ label: 'Kitchen Item' }, { label: 'Kitchen Item' }, { label: 'Kitchen Item' }] - } - ], - [ - { - label: 'Bedroom', - items: [{ label: 'Bedroom Item' }, { label: 'Bedroom Item' }] - }, - { - label: 'Outdoor', - items: [{ label: 'Outdoor Item' }, { label: 'Outdoor Item' }, { label: 'Outdoor Item' }] - } - ] - ] - }, - { - label: 'Sports', - icon: 'pi pi-fw pi-star', - items: [ - [ - { - label: 'Basketball', - items: [{ label: 'Basketball Item' }, { label: 'Basketball Item' }] - }, - { - label: 'Football', - items: [{ label: 'Football Item' }, { label: 'Football Item' }, { label: 'Football Item' }] - } - ], - [ - { - label: 'Tennis', - items: [{ label: 'Tennis Item' }, { label: 'Tennis Item' }] - } - ] - ] - } - ]; - - const panelMenuitems = [ - { - label: 'Customers', - icon: 'pi pi-fw pi-table', - items: [ - { - label: 'New', - icon: 'pi pi-fw pi-user-plus', - items: [ - { - label: 'Customer', - icon: 'pi pi-fw pi-plus' - }, - { - label: 'Duplicate', - icon: 'pi pi-fw pi-copy' - } - ] - }, - { - label: 'Edit', - icon: 'pi pi-fw pi-user-edit' - } - ] - }, - { - label: 'Orders', - icon: 'pi pi-fw pi-shopping-cart', - items: [ - { - label: 'View', - icon: 'pi pi-fw pi-list' - }, - { - label: 'Search', - icon: 'pi pi-fw pi-search' - } - ] - }, - { - label: 'Shipments', - icon: 'pi pi-fw pi-envelope', - items: [ - { - label: 'Tracker', - icon: 'pi pi-fw pi-compass' - }, - { - label: 'Map', - icon: 'pi pi-fw pi-map-marker' - }, - { - label: 'Manage', - icon: 'pi pi-fw pi-pencil' - } - ] - }, - { - label: 'Profile', - icon: 'pi pi-fw pi-user', - items: [ - { - label: 'Settings', - icon: 'pi pi-fw pi-cog' - }, - { - label: 'Billing', - icon: 'pi pi-fw pi-file' - } - ] - } - ]; - - const toggleMenu = (event: React.MouseEvent) => { - menu.current?.toggle(event); - }; - - const onContextRightClick = (event: React.MouseEvent) => { - contextMenu.current?.show(event); - }; - - const menubarEndTemplate = () => { - return ( - - - - - ); - }; - - return ( -
-
-
-
Menubar
- -
-
- -
-
-
Breadcrumb
- -
-
- -
-
-
Steps
- setActiveIndex(e.index)} readOnly={false} /> - {pathname === '/uikit/menu' ? ( -
- -

Personal Component Content via Child Route

-
- ) : ( - <>{children} - )} -
-
- -
-
-
TabMenu
- setActiveIndex(e.index)} /> - {pathname === '/uikit/menu' ? ( -
- -

Personal Component Content via Child Route

-
- ) : ( - <>{children} - )} -
-
- -
-
-
Tiered Menu
- -
-
- -
-
-
Plain Menu
- -
-
- -
-
-
Overlay Menu
- - -
- -
-
ContextMenu
- Right click to display. - -
-
- -
-
-
MegaMenu - Horizontal
- - -
MegaMenu - Vertical
- -
-
- -
-
-
PanelMenu
- -
-
-
- ); -}; - -export default MenuDemo; diff --git a/app/(main)/uikit/menu/payment/page.tsx b/app/(main)/uikit/menu/payment/page.tsx deleted file mode 100644 index 4f19d0b7..00000000 --- a/app/(main)/uikit/menu/payment/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client'; -import React from 'react'; -import Menu from '../page'; -function PaymentDemo() { - return ( - -
- -

Payment Component Content via Child Route

-
-
- ); -} - -export default PaymentDemo; diff --git a/app/(main)/uikit/menu/seat/page.tsx b/app/(main)/uikit/menu/seat/page.tsx deleted file mode 100644 index a4dd25cf..00000000 --- a/app/(main)/uikit/menu/seat/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client'; - -import React from 'react'; -import Menu from '../page'; -function SeatDemo() { - return ( - -
- -

Seat Component Content via Child Route

-
-
- ); -} - -export default SeatDemo; diff --git a/app/(main)/uikit/message/page.tsx b/app/(main)/uikit/message/page.tsx deleted file mode 100644 index 772cb852..00000000 --- a/app/(main)/uikit/message/page.tsx +++ /dev/null @@ -1,131 +0,0 @@ -'use client'; -import React, { useRef, useState } from 'react'; -import { Toast } from 'primereact/toast'; -import { Messages } from 'primereact/messages'; -import { Message } from 'primereact/message'; -import { InputText } from 'primereact/inputtext'; -import { Button } from 'primereact/button'; - -const MessagesDemo = () => { - const [username, setUsername] = useState(''); - const [email, setEmail] = useState(''); - const toast = useRef(null); - const message = useRef(null); - - const addSuccessMessage = () => { - message.current?.show({ severity: 'success', content: 'Message Detail' }); - }; - - const addInfoMessage = () => { - message.current?.show({ severity: 'info', content: 'Message Detail' }); - }; - - const addWarnMessage = () => { - message.current?.show({ severity: 'warn', content: 'Message Detail' }); - }; - - const addErrorMessage = () => { - message.current?.show({ severity: 'error', content: 'Message Detail' }); - }; - - const showSuccess = () => { - toast.current?.show({ - severity: 'success', - summary: 'Success Message', - detail: 'Message Detail', - life: 3000 - }); - }; - - const showInfo = () => { - toast.current?.show({ - severity: 'info', - summary: 'Info Message', - detail: 'Message Detail', - life: 3000 - }); - }; - - const showWarn = () => { - toast.current?.show({ - severity: 'warn', - summary: 'Warn Message', - detail: 'Message Detail', - life: 3000 - }); - }; - - const showError = () => { - toast.current?.show({ - severity: 'error', - summary: 'Error Message', - detail: 'Message Detail', - life: 3000 - }); - }; - - return ( -
-
-
-
Toast
-
- -
-
-
- -
-
-
Messages
-
-
- -
-
- -
-
-
Inline
-
- - setUsername(e.target.value)} required className="p-invalid" /> - -
-
- - setEmail(e.target.value)} required className="p-invalid" /> - -
-
-
- -
-
-
Help Text
-
- - - - Enter your username to reset your password. - -
-
-
-
- ); -}; - -export default MessagesDemo; diff --git a/app/(main)/uikit/misc/page.tsx b/app/(main)/uikit/misc/page.tsx deleted file mode 100644 index 92c72d8b..00000000 --- a/app/(main)/uikit/misc/page.tsx +++ /dev/null @@ -1,220 +0,0 @@ -'use client'; - -import React, { useState, useEffect, useRef } from 'react'; -import { ProgressBar } from 'primereact/progressbar'; -import { Button } from 'primereact/button'; -import { Badge } from 'primereact/badge'; -import { Tag } from 'primereact/tag'; -import { Avatar } from 'primereact/avatar'; -import { AvatarGroup } from 'primereact/avatargroup'; -import { Chip } from 'primereact/chip'; -import { Skeleton } from 'primereact/skeleton'; -import { ScrollPanel } from 'primereact/scrollpanel'; -import { ScrollTop } from 'primereact/scrolltop'; - -const MiscDemo = () => { - const [value, setValue] = useState(0); - const intervalRef = useRef(null); - - useEffect(() => { - const interval = setInterval(() => { - setValue((prevValue) => { - const newVal = prevValue + Math.floor(Math.random() * 10) + 1; - return newVal >= 100 ? 100 : newVal; - }); - }, 2000); - - intervalRef.current = interval; - - return () => { - clearInterval(intervalRef.current as NodeJS.Timeout); - intervalRef.current = null; - }; - }, []); - - return ( -
-
-
-
ProgressBar
-
-
- -
-
- -
-
-
-
-
-
-

Badge

-
Numbers
-
- - - - - -
- -
Positioned Badge
-
- - - - - - - - - -
- -
Button Badge
-
- - -
-
Sizes
-
- - - -
-
- -
-

Avatar

-
Avatar Group
- - - - - - - - - -
Label - Circle
-
- - - -
- -
Icon - Badge
- - - -
- -
-

ScrollTop

- -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae et leo duis ut diam. Ultricies mi quis hendrerit dolor magna eget est lorem. Amet - consectetur adipiscing elit ut. Nam libero justo laoreet sit amet. Pharetra massa massa ultricies mi quis hendrerit dolor magna. Est ultricies integer quis auctor elit sed vulputate. Consequat ac felis donec et. Tellus - orci ac auctor augue mauris. Semper feugiat nibh sed pulvinar proin gravida hendrerit lectus a. Tincidunt arcu non sodales neque sodales. Metus aliquam eleifend mi in nulla posuere sollicitudin aliquam ultrices. Sodales ut - etiam sit amet nisl purus. Cursus sit amet dictum sit amet. Tristique senectus et netus et malesuada fames ac turpis egestas. Et tortor consequat id porta nibh venenatis cras sed. Diam maecenas ultricies mi eget mauris. - Eget egestas purus viverra accumsan in nisl nisi. Suscipit adipiscing bibendum est ultricies integer. Mattis aliquam faucibus purus in massa tempor nec. -

- -
-
-
-
-
-

Tag

-
Tags
-
- - - - - -
- -
Pills
-
- - - - - -
- -
Icons
-
- - - - - -
-
- -
-

Chip

-
Basic
-
- - - - -
- -
Icon
-
- - - - -
- -
Image
-
- - - - -
- -
Styling
-
- - - - -
-
- -
-

Skeleton

-
-
- -
- - - -
-
- -
- - -
-
-
-
-
- ); -}; - -export default MiscDemo; diff --git a/app/(main)/uikit/overlay/page.tsx b/app/(main)/uikit/overlay/page.tsx deleted file mode 100644 index 173512dd..00000000 --- a/app/(main)/uikit/overlay/page.tsx +++ /dev/null @@ -1,222 +0,0 @@ -'use client'; - -import { Button } from 'primereact/button'; -import { Column } from 'primereact/column'; -import { confirmPopup, ConfirmPopup } from 'primereact/confirmpopup'; -import { DataTable, DataTableSelectEvent } from 'primereact/datatable'; -import { Dialog } from 'primereact/dialog'; -import { InputText } from 'primereact/inputtext'; -import { OverlayPanel } from 'primereact/overlaypanel'; -import { Sidebar } from 'primereact/sidebar'; -import { Toast } from 'primereact/toast'; -import React, { useEffect, useRef, useState } from 'react'; -import { ProductService } from '../../../../demo/service/ProductService'; -import type { Demo } from '@/types'; - -type ButtonEvent = React.MouseEvent; -const OverlayDemo = () => { - const [displayBasic, setDisplayBasic] = useState(false); - const [displayConfirmation, setDisplayConfirmation] = useState(false); - const [visibleLeft, setVisibleLeft] = useState(false); - const [visibleRight, setVisibleRight] = useState(false); - const [visibleTop, setVisibleTop] = useState(false); - const [visibleBottom, setVisibleBottom] = useState(false); - const [visibleFullScreen, setVisibleFullScreen] = useState(false); - const [products, setProducts] = useState([]); - const [selectedProduct, setSelectedProduct] = useState(null); - const op = useRef(null); - const op2 = useRef(null); - const toast = useRef(null); - - const accept = () => { - toast.current?.show({ - severity: 'info', - summary: 'Confirmed', - detail: 'You have accepted', - life: 3000 - }); - }; - - const reject = () => { - toast.current?.show({ - severity: 'error', - summary: 'Rejected', - detail: 'You have rejected', - life: 3000 - }); - }; - - const confirm = (event: React.MouseEvent) => { - confirmPopup({ - target: event.currentTarget, - message: 'Are you sure you want to proceed?', - icon: 'pi pi-exclamation-triangle', - accept, - reject - }); - }; - - useEffect(() => { - ProductService.getProductsSmall().then((data) => setProducts(data)); - }, []); - - const toggle = (event: ButtonEvent) => { - op.current?.toggle(event); - }; - - const toggleDataTable = (event: ButtonEvent) => { - op2.current?.toggle(event); - }; - - const formatCurrency = (value: number) => { - return value.toLocaleString('en-US', { - style: 'currency', - currency: 'USD' - }); - }; - - const onProductSelect = (event: DataTableSelectEvent) => { - op2.current?.hide(); - toast.current?.show({ - severity: 'info', - summary: 'Product Selected', - detail: event.data.name, - life: 3000 - }); - }; - - const onSelectionChange = (e: any): void => { - setSelectedProduct(e.value as Demo.Product); - }; - - const basicDialogFooter =
-
-
-
-
Overlay Panel
-
-
-
-
-
-
-
-
- -
-
-
Confirmation
-
-
-
Sidebar
- setVisibleLeft(false)} baseZIndex={1000}> -

Left Sidebar

-
- - setVisibleRight(false)} baseZIndex={1000} position="right"> -

Right Sidebar

-
- - setVisibleTop(false)} baseZIndex={1000} position="top"> -

Top Sidebar

-
- - setVisibleBottom(false)} baseZIndex={1000} position="bottom"> -

Bottom Sidebar

-
- - setVisibleFullScreen(false)} baseZIndex={1000} fullScreen> -

Full Screen

-
- -
-
- -
-
-
Tooltip
-
- - - - -
-
-
-
- - -
-
ConfirmPopup
- - -
-
-
- - ); -}; - -export default OverlayDemo; diff --git a/app/(main)/uikit/panel/page.tsx b/app/(main)/uikit/panel/page.tsx deleted file mode 100644 index 5a852b75..00000000 --- a/app/(main)/uikit/panel/page.tsx +++ /dev/null @@ -1,233 +0,0 @@ -'use client'; - -import React, { useRef } from 'react'; -import { Toolbar } from 'primereact/toolbar'; -import { Button } from 'primereact/button'; -import { SplitButton } from 'primereact/splitbutton'; -import { Accordion, AccordionTab } from 'primereact/accordion'; -import { TabView, TabPanel } from 'primereact/tabview'; -import { Panel } from 'primereact/panel'; -import { Fieldset } from 'primereact/fieldset'; -import { Card } from 'primereact/card'; -import { Divider } from 'primereact/divider'; -import { InputText } from 'primereact/inputtext'; -import { Splitter, SplitterPanel } from 'primereact/splitter'; -import { Menu } from 'primereact/menu'; - -const PanelDemo = () => { - const menu1 = useRef(null); - const toolbarItems = [ - { - label: 'Save', - icon: 'pi pi-check' - }, - { - label: 'Update', - icon: 'pi pi-sync' - }, - { - label: 'Delete', - icon: 'pi pi-trash' - }, - { - label: 'Home Page', - icon: 'pi pi-home' - } - ]; - - const toolbarLeftTemplate = () => { - return ( - <> -
- ); - - return ( -
-
-
-
Toolbar
- -
-
-
-
-
AccordionPanel
- - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea - commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim - id est laborum. -

-
- -

- Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. - Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Consectetur, adipisci velit, sed quia non numquam eius modi. -

-
- -

- At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt - in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo - minus. -

-
-
-
-
-
TabView
- - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea - commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim - id est laborum. -

-
- -

- Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. - Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Consectetur, adipisci velit, sed quia non numquam eius modi. -

-
- -

- At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt - in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo - minus. -

-
-
-
-
-
-
-
Panel
- -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est - laborum. -

-
-
-
-
Fieldset
-
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est - laborum. -

-
-
- -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -

-
-
- -
-
-
Divider
-
-
-
-
- - -
-
- - -
- -
-
-
- - OR - -
-
-

- Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. - Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Consectetur, adipisci velit, sed quia non numquam eius modi. -

- - - Badge - - -

- At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt - in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo - minus. -

- - - - - -

- Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut - reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. Donec vel volutpat ipsum. Integer nunc magna, posuere ut tincidunt eget, egestas vitae sapien. Morbi dapibus luctus odio. -

-
-
-
-
- -
-
-
Splitter
- - -
Panel 1
-
- - - -
Panel 2
-
- -
Panel 3
-
-
-
-
-
-
-
- ); -}; - -export default PanelDemo; diff --git a/app/(main)/uikit/table/page.tsx b/app/(main)/uikit/table/page.tsx deleted file mode 100644 index cb399065..00000000 --- a/app/(main)/uikit/table/page.tsx +++ /dev/null @@ -1,464 +0,0 @@ -'use client'; -import { CustomerService } from '../../../../demo/service/CustomerService'; -import { ProductService } from '../../../../demo/service/ProductService'; -import { FilterMatchMode, FilterOperator } from 'primereact/api'; -import { Button } from 'primereact/button'; -import { Calendar } from 'primereact/calendar'; -import { Column, ColumnFilterApplyTemplateOptions, ColumnFilterClearTemplateOptions, ColumnFilterElementTemplateOptions } from 'primereact/column'; -import { DataTable, DataTableExpandedRows, DataTableFilterMeta } from 'primereact/datatable'; -import { Dropdown } from 'primereact/dropdown'; -import { InputNumber } from 'primereact/inputnumber'; -import { InputText } from 'primereact/inputtext'; -import { MultiSelect } from 'primereact/multiselect'; -import { ProgressBar } from 'primereact/progressbar'; -import { Rating } from 'primereact/rating'; -import { Slider } from 'primereact/slider'; -import { ToggleButton } from 'primereact/togglebutton'; -import { TriStateCheckbox } from 'primereact/tristatecheckbox'; -import { classNames } from 'primereact/utils'; -import React, { useEffect, useState } from 'react'; -import type { Demo } from '@/types'; - -const TableDemo = () => { - const [customers1, setCustomers1] = useState([]); - const [customers2, setCustomers2] = useState([]); - const [customers3, setCustomers3] = useState([]); - const [filters1, setFilters1] = useState({}); - const [loading1, setLoading1] = useState(true); - const [loading2, setLoading2] = useState(true); - const [idFrozen, setIdFrozen] = useState(false); - const [products, setProducts] = useState([]); - const [globalFilterValue1, setGlobalFilterValue1] = useState(''); - const [expandedRows, setExpandedRows] = useState([]); - const [allExpanded, setAllExpanded] = useState(false); - - const representatives = [ - { name: 'Amy Elsner', image: 'amyelsner.png' }, - { name: 'Anna Fali', image: 'annafali.png' }, - { name: 'Asiya Javayant', image: 'asiyajavayant.png' }, - { name: 'Bernardo Dominic', image: 'bernardodominic.png' }, - { name: 'Elwin Sharvill', image: 'elwinsharvill.png' }, - { name: 'Ioni Bowcher', image: 'ionibowcher.png' }, - { name: 'Ivan Magalhaes', image: 'ivanmagalhaes.png' }, - { name: 'Onyama Limba', image: 'onyamalimba.png' }, - { name: 'Stephen Shaw', image: 'stephenshaw.png' }, - { name: 'XuXue Feng', image: 'xuxuefeng.png' } - ]; - - const statuses = ['unqualified', 'qualified', 'new', 'negotiation', 'renewal', 'proposal']; - - const clearFilter1 = () => { - initFilters1(); - }; - - const onGlobalFilterChange1 = (e: React.ChangeEvent) => { - const value = e.target.value; - let _filters1 = { ...filters1 }; - (_filters1['global'] as any).value = value; - - setFilters1(_filters1); - setGlobalFilterValue1(value); - }; - - const renderHeader1 = () => { - return ( -
-
- ); - }; - - useEffect(() => { - setLoading2(true); - - CustomerService.getCustomersLarge().then((data) => { - setCustomers1(getCustomers(data)); - setLoading1(false); - }); - CustomerService.getCustomersLarge().then((data) => { - setCustomers2(getCustomers(data)); - setLoading2(false); - }); - CustomerService.getCustomersMedium().then((data) => setCustomers3(data)); - ProductService.getProductsWithOrdersSmall().then((data) => setProducts(data)); - - initFilters1(); - }, []); - - const balanceTemplate = (rowData: Demo.Customer) => { - return ( -
- {formatCurrency(rowData.balance as number)} -
- ); - }; - - const getCustomers = (data: Demo.Customer[]) => { - return [...(data || [])].map((d) => { - d.date = new Date(d.date); - return d; - }); - }; - - const formatDate = (value: Date) => { - return value.toLocaleDateString('en-US', { - day: '2-digit', - month: '2-digit', - year: 'numeric' - }); - }; - - const formatCurrency = (value: number) => { - return value.toLocaleString('en-US', { - style: 'currency', - currency: 'USD' - }); - }; - - const initFilters1 = () => { - setFilters1({ - global: { value: null, matchMode: FilterMatchMode.CONTAINS }, - name: { - operator: FilterOperator.AND, - constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] - }, - 'country.name': { - operator: FilterOperator.AND, - constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] - }, - representative: { value: null, matchMode: FilterMatchMode.IN }, - date: { - operator: FilterOperator.AND, - constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] - }, - balance: { - operator: FilterOperator.AND, - constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] - }, - status: { - operator: FilterOperator.OR, - constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] - }, - activity: { value: null, matchMode: FilterMatchMode.BETWEEN }, - verified: { value: null, matchMode: FilterMatchMode.EQUALS } - }); - setGlobalFilterValue1(''); - }; - - const countryBodyTemplate = (rowData: Demo.Customer) => { - return ( - - flag - {rowData.country.name} - - ); - }; - - const filterClearTemplate = (options: ColumnFilterClearTemplateOptions) => { - return ; - }; - - const filterApplyTemplate = (options: ColumnFilterApplyTemplateOptions) => { - return ; - }; - - const representativeBodyTemplate = (rowData: Demo.Customer) => { - const representative = rowData.representative; - return ( - - {representative.name} ((e.target as HTMLImageElement).src = 'https://www.primefaces.org/wp-content/uploads/2020/05/placeholder.png')} - width={32} - style={{ verticalAlign: 'middle' }} - /> - {representative.name} - - ); - }; - - const representativeFilterTemplate = (options: ColumnFilterElementTemplateOptions) => { - return ( - <> -
Agent Picker
- options.filterCallback(e.value)} optionLabel="name" placeholder="Any" className="p-column-filter" /> - - ); - }; - - const representativesItemTemplate = (option: any) => { - return ( -
- {option.name} - {option.name} -
- ); - }; - - const dateBodyTemplate = (rowData: Demo.Customer) => { - return formatDate(rowData.date); - }; - - const dateFilterTemplate = (options: ColumnFilterElementTemplateOptions) => { - return options.filterCallback(e.value, options.index)} dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy" mask="99/99/9999" />; - }; - - const balanceBodyTemplate = (rowData: Demo.Customer) => { - return formatCurrency(rowData.balance as number); - }; - - const balanceFilterTemplate = (options: ColumnFilterElementTemplateOptions) => { - return options.filterCallback(e.value, options.index)} mode="currency" currency="USD" locale="en-US" />; - }; - - const statusBodyTemplate = (rowData: Demo.Customer) => { - return {rowData.status}; - }; - - const statusFilterTemplate = (options: ColumnFilterElementTemplateOptions) => { - return options.filterCallback(e.value, options.index)} itemTemplate={statusItemTemplate} placeholder="Select a Status" className="p-column-filter" showClear />; - }; - - const statusItemTemplate = (option: any) => { - return {option}; - }; - - const activityBodyTemplate = (rowData: Demo.Customer) => { - return ; - }; - - const activityFilterTemplate = (options: ColumnFilterElementTemplateOptions) => { - return ( - - options.filterCallback(e.value)} range className="m-3"> -
- {options.value ? options.value[0] : 0} - {options.value ? options.value[1] : 100} -
-
- ); - }; - - const verifiedBodyTemplate = (rowData: Demo.Customer) => { - return ( - - ); - }; - - const verifiedFilterTemplate = (options: ColumnFilterElementTemplateOptions) => { - return options.filterCallback(e.value)} />; - }; - - const toggleAll = () => { - if (allExpanded) collapseAll(); - else expandAll(); - }; - - const expandAll = () => { - let _expandedRows = {} as { [key: string]: boolean }; - products.forEach((p) => (_expandedRows[`${p.id}`] = true)); - - setExpandedRows(_expandedRows); - setAllExpanded(true); - }; - - const collapseAll = () => { - setExpandedRows([]); - setAllExpanded(false); - }; - - const amountBodyTemplate = (rowData: Demo.Customer) => { - return formatCurrency(rowData.amount as number); - }; - - const statusOrderBodyTemplate = (rowData: Demo.Customer) => { - return {rowData.status}; - }; - - const searchBodyTemplate = () => { - return +

{translations.videoInstructionsTitle}

+
+
+ {videoValues?.map((item, idx) => { + return ( +
+
+ +
+

{item?.title}

+
+ ); + })} +
+ + ); +} diff --git a/app/(student)/layout.tsx b/app/(student)/layout.tsx new file mode 100644 index 00000000..eafb4211 --- /dev/null +++ b/app/(student)/layout.tsx @@ -0,0 +1,14 @@ +import StudentLayout from "@/layout/StudentLayout"; + +export default function LayoutStudent({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ {/* {children} */} + {children} +
+ ); +} \ No newline at end of file diff --git a/app/(student)/studentHome/page.tsx b/app/(student)/studentHome/page.tsx new file mode 100644 index 00000000..a18835e1 --- /dev/null +++ b/app/(student)/studentHome/page.tsx @@ -0,0 +1,169 @@ +'use client'; + +import useMediaQuery from '@/hooks/useMediaQuery'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { fetchStudentActivity, fetchStudentStatistic } from '@/services/studentMain'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { useContext, useEffect, useRef, useState } from 'react'; +import ActivityPage from '@/app/components/Contribution'; +import { fetchStudentImg } from '@/services/student/studentpage'; +import MyDateTime from '@/app/components/MyDateTime'; +import { OptionsType } from '@/types/OptionsType'; +import { ContributionDay } from '@/types/ContributionDay'; +import Link from 'next/link'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +interface StudentStatistic { + all_active_dates: number; + last_visit: string; + streak: number; +} + +export default function StudentHome() { + const { user, contextNotifications } = useContext(LayoutContext); + const ref = useRef(null); + const media = useMediaQuery('(max-width: 640px)'); + const { translations } = useLocalization(); + + const [loading, setLoading] = useState(false); + const [studentImg, setStudentImg] = useState<{ image_url: string; id: string } | null>(null); + const [studentStatistic, setStudentStatistic] = useState(null); + const [contribution, setContribution] = useState(null); + const [telegramData, setTelegramData] = useState<{ direct_link: string; qr_code_base64: string } | null>(null); + const [showTelegramDialog, setShowTelegramDialog] = useState(false); + + const options: OptionsType = { + year: '2-digit', + month: 'short', // 'long', 'short', 'numeric' + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, // 24-часовой формат + timeZone: 'UTC' + }; + + const handleFetchStudentImg = async () => { + const data = await fetchStudentImg(); + if (data && data?.success) { + setStudentImg(data?.data); + } + }; + + const handleFetchStudentStatistic = async () => { + const data = await fetchStudentStatistic(); + if (data && data?.success) { + setStudentStatistic(data?.data); + } + }; + + const handleFetchStudentActivity = async () => { + const data = await fetchStudentActivity(); + if (data && data?.length) { + setContribution(data); + } + }; + + useEffect(() => { + if (media) { + if (ref.current) { + ref.current.scrollLeft = ref.current.scrollWidth; + } + } + }, [media]); + + useEffect(() => { + if (user?.is_student) { + // handleFetchLessons(); + handleFetchStudentImg(); + handleFetchStudentStatistic(); + handleFetchStudentActivity(); + } + }, [user]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Top Section: Greeting and Stats */} +
+ {/* 1. Greeting Section */} +
+
+
+ {/* Контейнер-рамка */} +
+ {translations.photo} +
+
+
+

{translations.welcome}, {user?.name || translations.studentBtn}

+
+

+ {user?.last_name} {user?.name} {user?.father_name} +

+
+ {contextNotifications?.length} + {translations.notifications} +
+
+ + {translations.trainingPlan} + +
+
+ {/* Decorative background element */} +
+ +
+
+ + {/* 2. Stats Section */} +
+
+

{translations.upcomingEvents}

+
+

{translations.noUpcomingEvents}

+
+
+
+
+ + {/* {lessonsData && Object.keys(lessonsData).length > 0 ? ( */} +
+ {/* activity */} +
+

+ {translations.activity} +

+ + +
+
+ {} + {translations.lastVisit} +
+
+ {studentStatistic?.streak} + {translations.daysVisitedStreak} +
+
+ {studentStatistic?.all_active_dates} + {translations.daysVisitedTotal} +
+
+
+
+
+ ); +} diff --git a/app/(student)/teaching/[subject_id]/[id_edu_year]/page.tsx b/app/(student)/teaching/[subject_id]/[id_edu_year]/page.tsx new file mode 100644 index 00000000..500da0cf --- /dev/null +++ b/app/(student)/teaching/[subject_id]/[id_edu_year]/page.tsx @@ -0,0 +1,391 @@ +'use client'; + +import StudentInfoCard from '@/app/components/lessons/StudentInfoCard'; +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { fetchItemsLessons, fetchMainLesson, fetchSubjects } from '@/services/studentMain'; +import { lessonType } from '@/types/lessonType'; +import { useParams } from 'next/navigation'; +import { Accordion, AccordionTab } from 'primereact/accordion'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import React, { useContext, useEffect, useState } from 'react'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; +import MainTitle from '@/app/components/titles/MainTitle'; + +export default function StudentLesson() { + // types + interface subjectType { + id_curricula: number; + course_ids: number[]; + streams: number[]; + } + + interface CourseType { + id: number; + connections: { subject_type: string; id: number; user_id: number | null; id_stream: number }[]; + title: string; + description: string; + user: { last_name: string; name: string; father_name: string }; + lessons: lessonType[]; + } + + // type HeaderTemplateOptions = Parameters['headerTemplate']>>[0]; + + const { subject_id, id_edu_year } = useParams<{subject_id: string; id_edu_year: string}>(); + const params = new URLSearchParams(); + + const { setMessage, setForumValues, contextLastStepVisit, setContextLastStepVisit, contextLastSubjectPageVisit, setContextLastSubjectPageVisit } = useContext(LayoutContext); + const showError = useErrorMessage(); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + + const [main_id, setMain_id] = useState(null); + + const [skeleton, setSkeleton] = useState(false); + const [hasLessons, setHasLessons] = useState(false); + const [lessons, setLessons] = useState>({ + 1: { semester: { name_kg: '' } } + }); + const [courses, setCourses] = useState([]); + const [hasThemes, setHasThemes] = useState(false); + const [activeIndexes, setActiveIndexes] = useState>({}); + const [accordionIndex, setAccordionIndex] = useState({ index: null }); + const [mainProgressSpinner, setMainProgressSpinner] = useState(false); + + const handleMainLesson = async (lesson_id: number, stream_id: number) => { + const data = await fetchMainLesson(lesson_id, stream_id); + // Возвращаем данные или null/пустой массив + if (data && data.length > 0) { + return data; + } else { + if (data?.response?.data && data?.response?.status === '400') { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } }); + } + } + return []; + }; + + // Вам понадобится эта функция для получения ID урока + const getLessonIdByCourseAndIndex = (course: any, index: number) => { + // Убедитесь, что lessonType включает поле steps: any[] + return course.lessons[index]?.id; + }; + + const handleTabChange = async (courseInside: CourseType[], courseId: number, e: any, sendActiveIndex: boolean) => { + // 1. Обновление активного индекса (ОК) + if (sendActiveIndex) { + setActiveIndexes((prev) => ({ + ...prev, + [courseId]: e.index // e.index - это индекс темы (AccordionTab) + })); + setAccordionIndex({ index: e.index }); + } + + const course = courseInside.find((c) => c.id === courseId); + + // Если вкладка закрывается (e.index == null или -1), + // нет необходимости в загрузке данных + if (course && e.index !== null && e.index >= 0) { + const lessonId = getLessonIdByCourseAndIndex(course, e.index); + const stream = course.connections[0]; + + // 2. Вызываем новую handleMainLesson и получаем данные + if (lessonId && stream) { + // 1. Устанавливаем статус загрузки для конкретного урока + setCourses((prevCourses) => + prevCourses.map((c) => { + if (c.id === courseId) { + return { + ...c, + lessons: c.lessons.map( + (l) => (l.id === lessonId ? { ...l, isLoadingSteps: true } : l) // 💡 ВКЛЮЧАЕМ + ) + }; + } + return c; + }) + ); + + const newSteps = await handleMainLesson(lessonId, stream.id_stream); + if (newSteps) { + // 3. Обновляем состояние courses: добавляем steps к нужному уроку + setCourses((prevCourses) => + prevCourses.map((c) => { + // Находим нужный курс + if (c.id === courseId) { + return { + ...c, + lessons: c.lessons.map((l) => + // Находим нужный урок и обновляем его шаги + l.id === lessonId ? { ...l, steps: newSteps, isLoadingSteps: false } : l + ) + }; + } + return c; + }) + ); + } + } + } + }; + + // fetch lessons + const handleFetchLessons = async () => { + setSkeleton(true); + const data = await fetchItemsLessons(id_edu_year ? Number(id_edu_year) : 0); + if (data && data?.success) { + // валидность проверить + setLessons(data?.data); + setHasLessons(false); + } else { + setHasLessons(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.data?.response?.status) { + showError(data?.data.response.status); + } + } + setSkeleton(false); + }; + + // Запрос курса, типа уроков (лк,лб) + const handleFetchSubject = async (subject: subjectType) => { + setMainProgressSpinner(true); + params.append('id_curricula', String(subject?.id_curricula)); + subject?.streams.forEach((i) => params.append('streams[]', String(i))); + subject?.course_ids.forEach((i) => params.append('course_ids[]', String(i))); + const data = await fetchSubjects(params); + setSkeleton(true); + if (data && Array.isArray(data)) { + setCourses(data); + if (data && data?.length > 0) { + const courseId = data[0].id; + if (courseId) { + // проверить контекст если у него есть значит забираем от него + if (contextLastStepVisit && contextLastStepVisit.course_id) { + handleTabChange(data, contextLastStepVisit.course_id, { index: contextLastStepVisit.index }, true); + } else if (data[0].lessons && data[0].lessons[0]?.active) { + handleTabChange(data, courseId, { index: 0 }, true); + } + } + } + setHasThemes(false); + setSkeleton(false); + } else { + setHasThemes(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + setSkeleton(false); + } + setMainProgressSpinner(false); + }; + + // НАХОДИМ ПО ИД КРУКЛА НУЖНЫЙ ЭЛЕМЕНТ МАССИВА И ПРИСВАИВАЕМ В main_id ОБЪЕКТ + + // Просим предметы для получения конкретного из них + useEffect(() => { + handleFetchLessons(); + }, []); + + // Из предметов получаю выбранный курс в main_id + useEffect(() => { + const lessonArray = Object.values(lessons); + // console.log(lessonArray) + let search_id: predmetType | null = null; + if (lessonArray && Array.isArray(lessonArray)) { + for (let i = 0; i < lessonArray?.length; i++) { + const lessonItemArray = Object.values(lessonArray[i]); + // console.log(lessonItemArray); + + search_id = lessonItemArray.find((item: any) => item?.id_curricula == subject_id); + if (search_id && search_id != null) { + break; + } + } + if (search_id) { + setMain_id(search_id); + } + } + }, [lessons, subject_id]); + + // НАХОДИМ ПО ИД КРУКЛА НУЖНЫЙ ЭЛЕМЕНТ МАССИВА И ПРИСВАИВАЕМ В main_id ОБЪЕКТ + + useEffect(() => { + if (main_id && main_id != null) { + const forSubject: subjectType = { id_curricula: main_id?.id_curricula, course_ids: main_id?.course_ids, streams: main_id?.streams.map((i: { id: number }) => i.id) }; + handleFetchSubject(forSubject); + } + }, [main_id]); + + if (mainProgressSpinner) + return ( +
+ +
+ ); + + return ( +
+ {hasLessons ? ( + + ) : ( + <> + {/*

{translations.courses}

*/} + {translations.courses} + + {skeleton ? ( +
+ +
+ ) : hasThemes ? ( + + ) : ( + Array.isArray(courses) && + courses?.map((course) => { + return ( +
+
+

+ {translations.courseName}: {getLocalized(course, 'title') || course?.title} +

+

+ {translations.teacher}: {course?.user.last_name} {course?.user.name}{' '} + {course?.user.father_name ? course?.user.father_name[0] && course?.user.father_name : ''} +

+ {course?.connections[0]?.subject_type && ( +

+ {translations.studyType}:{' '} + {course?.connections[0]?.subject_type === 'Лк' ? 'Лекция' : course?.connections[0]?.subject_type === 'Лб' ? 'Лабораторные занятия' : ''} +

+ )} +
+
+ { + setContextLastStepVisit({ course_id: course.id, index: e.index }); + handleTabChange(courses, course.id, e, true); + }} + multiple={false} + > + {course?.lessons.map((lesson, idx) => { + const contentPresence = lesson?.steps?.filter((content) => content.content); + const sortedSteps = contentPresence?.sort((a, b) => { + const isAForum = a?.type?.name === 'forum'; + const isBForum = b?.type?.name === 'forum'; + + if (isAForum && !isBForum) { + return 1; // 'a' (форум) идет после 'b' + } + if (!isAForum && isBForum) { + return -1; // 'b' (форум) идет после 'a' + } + return 0; // Сохраняем относительный порядок + }); + + return ( + + + {idx + 1}. {translations.theme}: {lesson.title} + + {!lesson?.active && ( +
+ {translations.availableFrom} + + {lesson?.from} - {lesson?.to} + +
+ )} + + } + key={lesson.id} + className={`w-full p-accordion my-accardion-icon ${!lesson?.active ? 'opacity-50 pointer-events-none' : ''}`} + style={{ width: '100%', backgroundColor: 'white' }} + > +
+ {/* Используем lesson.steps, который был обновлен в handleTabChange */} + {lesson?.isLoadingSteps ? ( + + ) : sortedSteps && sortedSteps?.length > 0 ? ( + sortedSteps.map( + ( + item: { + id: number; + chills: boolean; + type: { name: string; logo: string }; + content: { id: number; title: string; description: string; url: string; document: string; document_path: string }; + id_parent?: number | null; + score: number; + my_score: number | null; + }, + idx + ) => { + if (item.content == null) { + return null; + } + + return ( +
0 ? 'my-border-top' : ''}`}> + { + handleTabChange(courses, course.id, accordionIndex, true); + }} + contentId={item?.content?.id} + id_parent={item?.id_parent || null} + forumValueAdd={() => { + setForumValues({ description: item?.content.title || '', userInfo: { userName: course?.user?.name, userLastName: course?.user?.last_name } }); + localStorage.setItem( + 'forumValues', + JSON.stringify({ description: item?.content.title || '', userInfo: { userName: course?.user?.name, userLastName: course?.user?.last_name } }) + ); + }} + lessonItem={item} + id_edu_year={id_edu_year} + /> +
+ ); + } + ) + ) : ( +

{translations.noData}

+ )} +
+
+ ); + })} +
+
+
+ ); + }) + )} + + )} +
+ ); +} diff --git a/app/(student)/teaching/lessonView/[lesson_id]/[subject_id]/[stream_id]/[id]/[id_edu_year]/page.tsx b/app/(student)/teaching/lessonView/[lesson_id]/[subject_id]/[stream_id]/[id]/[id_edu_year]/page.tsx new file mode 100644 index 00000000..89c45901 --- /dev/null +++ b/app/(student)/teaching/lessonView/[lesson_id]/[subject_id]/[stream_id]/[id]/[id_edu_year]/page.tsx @@ -0,0 +1,871 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +const PDFreader = dynamic(() => import('@/app/components/pdfComponents/PDFreader'), { ssr: false }); + +import { NotFound } from '@/app/components/NotFound'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { statusView } from '@/services/notifications'; +import { fetchItemsLessons, fetchMainLesson, fetchStudentSteps, fetchSubjects, stepPractica, stepTest } from '@/services/studentMain'; +import { docValueType } from '@/types/docValueType'; +import { lessonType } from '@/types/lessonType'; +import { mainStepsType } from '@/types/mainStepType'; +import { useParams } from 'next/navigation'; +import { Button } from 'primereact/button'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { useContext, useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { StepApi } from '@/types/Step/StepApi/StepApi'; +import Link from 'next/link'; +import AnswersTable from '@/app/components/tables/AnswersTable'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; + +export default function LessonTest() { + // types + interface subjectType { + id_curricula: number; + course_ids: number[]; + streams: number[]; + } + + interface LessonNavigationUi { + lesson: lessonType; + lessons: lessonType[]; + } + + interface StepNavigationUi { + stepsLength: number; + currentStepPos: number; + nextStep: StepApi; + prevStep: StepApi; + } + + interface ReportStep extends mainStepsType { + my_score?: number | null; + details: { is_correct: boolean }[]; + } + + const { lesson_id, subject_id, stream_id, id, id_edu_year } = useParams(); + const params = new URLSearchParams(); + + const router = useRouter(); + const media = useMediaQuery('(max-width: 640px)'); + const showError = useErrorMessage(); + const { user, setMessage, contextNotificationId, setContextNotificationId, handleNotifications, contextLastSubjectPageVisit, setContextLastSubjectPageVisit } = useContext(LayoutContext); + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + + const [steps, setMainSteps] = useState(null); + const [hasSteps, setHasSteps] = useState(false); + const [progressSpinner, setProgressSpinner] = useState(false); + const [mainProgressSpinner, setMainProgressSpinner] = useState(false); + const [type, setType] = useState(''); + const [practica, setPractica] = useState<{ + content?: { document: string; document_path: string; description: string | null; title: string; link: string; url: string; content: string; answers: [{ text: string; is_correct: boolean; id: number | null }]; score: number }; + } | null>(null); + const [test, setTests] = useState(null); + const [answer, setAnswer] = useState<{ id: number | null; text: string; is_correct: boolean }[] | null>(null); + const [selectedAnswer, setSelectedAnswer] = useState(false); + const [answerCheck, setAnswerCheck] = useState(false); + const [lessons, setLessons] = useState>({ + 1: { semester: { name_kg: '' } } + }); + const [lessonName, setLessonName] = useState(''); + const [lessonNavigation, setLessonNavigation] = useState(null); + const [stepNavigation, setStepNavigation] = useState(null); + const [nextLesson, setNextLesson] = useState(null); + const [prevLesson, setPrevLesson] = useState(null); + const [navigationStepId, setNavigationStepId] = useState(null); + const [courseInfo, setCoursesInfo] = useState<{ title: string; description: string; image: string } | null>(null); + const [main_id, setMain_id] = useState(null); + const [courses, setCourses] = useState< + { + id: number; + connections: { subject_type: string; id: number; user_id: number | null; id_stream: number }[]; + title: string; + description: string; + image: string; + user: { last_name: string; name: string; father_name: string }; + lessons: lessonType[]; + }[] + >([]); + const [docValue, setDocValue] = useState({ + title: '', + description: '', + file: null + }); + + // document + const [document, setDocument] = useState(null); + + // link + const [link, setLink] = useState(null); + + // video + const [video, setVideo] = useState(null); + const [preview, setPreview] = useState(false); + const [videoLink, setVideoLink] = useState(''); + + // fetch lessons + const handleFetchLessons = async () => { + setMainProgressSpinner(true); + const data = await fetchItemsLessons(Number(id_edu_year)); + if (data && data?.success) { + // валидность проверить + setLessons(data?.data); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.data?.response?.status) { + showError(data?.data.response.status); + } + } + setMainProgressSpinner(false); + }; + + const handleStatusView = async (notification_id: number | null) => { + if (notification_id) { + const data = await statusView(Number(notification_id)); + if (user?.is_working || user?.is_student) { + handleNotifications(); + } + setContextNotificationId(null); + } + }; + + const handleStep = async () => { + const data = await fetchStudentSteps(Number(id), Number(stream_id)); + if (data?.success) { + if (!data?.step?.content || data?.step?.content == null) { + setHasSteps(true); + } else { + setHasSteps(false); + setMainSteps(data.step); + } + } else { + setHasSteps(true); + } + }; + + // Запрос курса, типа уроков (лк,лб) + const handleFetchSubject = async (subject: subjectType) => { + params.append('id_curricula', String(subject.id_curricula)); + subject.streams.forEach((i) => params.append('streams[]', String(i))); + subject.course_ids.forEach((i) => params.append('course_ids[]', String(i))); + + const data = await fetchSubjects(params); + if (data) { + setCourses(data); + // setHasThemes(false); + } else { + // setHasThemes(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const handleVideoCall = (value: string | null) => { + setPreview(true); + + if (!value) { + setPreview(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: '' } + }); + } + + const url = new URL(typeof value === 'string' ? value : ''); + let videoId = null; + + if (url.hostname === 'youtu.be') { + // короткая ссылка, видео ID — в пути + videoId = url.pathname.slice(1); // убираем первый слеш + } else if (url.hostname === 'www.youtube.com' || url.hostname === 'youtube.com') { + // стандартная ссылка, видео ID в параметре v + videoId = url.searchParams.get('v'); + } + + if (!videoId) { + setPreview(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: '' } + }); + return null; // не удалось получить ID + } + // return `https://www.youtube.com/embed/${videoId}`; + setVideoLink(`https://www.youtube.com/embed/${videoId}`); + setPreview(false); + // setVisisble(true); + }; + + const handleAddTest = async () => { + setProgressSpinner(true); + const isCorrect = answer?.filter((item) => item.is_correct); + const data = await stepTest(steps && steps?.id, steps?.connections?.id_stream, (isCorrect && isCorrect[0]?.id) || null); + + if (data?.success) { + setProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'success', summary: '', detail: data?.message } + }); + handleStep(); + } else { + setProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: '' } + }); + handleStep(); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + const handleAddPractica = async () => { + setProgressSpinner(true); + const data = await stepPractica(steps && steps?.id, steps?.connections?.id_stream, docValue.file); + if (data?.success) { + setProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'success', summary: '', detail: data?.message } + }); + handleStep(); + } else { + setProgressSpinner(false); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: '' } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + // Вызываем список шагов + const handleMainLesson = async (lesson_id: number, stream_id: number) => { + const data = await fetchMainLesson(lesson_id, stream_id); + // Возвращаем данные или null/пустой массив + if (data && data.length > 0) { + return data; + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.dataTemporarilyUnavailable } }); + return []; + } + }; + + // Вызываем список шагов для prev , next темы + const handleLessonRouterPush = async (lesson_id: number, stream_id: number) => { + const data: any = await handleMainLesson(lesson_id, stream_id); + const validSteps = data?.filter((item: { id_parent: number | null }) => item?.id_parent); + if (validSteps && validSteps.length > 0) { + // пока состояние не используется + setNavigationStepId(validSteps[0]?.id); + if (validSteps[0]?.id) router.push(`/teaching/lessonView/${lesson_id}/${subject_id}/${stream_id}/${validSteps[0].id}/${id_edu_year}`); + } else { + setMessage({ state: true, value: { severity: 'error', summary: translations.error, detail: translations.noData } }); + } + }; + + useEffect(() => { + if (steps?.type.name === 'document') { + setType(steps?.type.name); + setDocument(steps); + } else if (steps?.type.name === 'link') { + setType(steps?.type.name); + setLink(steps); + } else if (steps?.type.name === 'practical') { + setType(steps?.type.name); + setPractica(steps); + } else if (steps?.type.name === 'test') { + setType(steps?.type.name); + setTests(steps); + setAnswer(steps?.content?.answers || []); + } else if (steps?.type.name === 'video') { + setType(steps?.type.name); + setVideo(steps); + } + }, [steps]); + + // Из предметов получаю выбранный курс в main_id + useEffect(() => { + const lessonArray = Object.values(lessons); + // console.log(lessonArray) + let search_id: predmetType | null = null; + if (lessonArray && Array.isArray(lessonArray)) { + for (let i = 0; i < lessonArray.length; i++) { + const lessonItemArray = Object.values(lessonArray[i]); + // console.log(lessonItemArray); + + search_id = lessonItemArray.find((item: any) => item.id_curricula == subject_id); + if (search_id && search_id != null) { + break; + } + } + if (search_id) { + setMain_id(search_id); + } + } + }, [lessons]); + + useEffect(() => { + if (main_id && main_id != null) { + const forSubject: subjectType = { id_curricula: main_id?.id_curricula, course_ids: main_id?.course_ids, streams: main_id?.streams.map((i: { id: number }) => i.id) }; + handleFetchSubject(forSubject); + } + }, [main_id]); + + useEffect(() => { + if (video?.content?.link) { + handleVideoCall(video.content.link); + } + }, [video]); + + useEffect(() => { + if (lesson_id) { + const forLesson = courses?.find((item) => { + return item?.lessons.find((j) => { + if (j?.id === Number(lesson_id)) { + setLessonName(j?.title || ''); + setLessonNavigation({ lessons: item.lessons, lesson: j }); + } + return j?.id === Number(lesson_id); + }); + }); + // if (forLesson && forLesson?.lessons) { + // setContextNewStudentThemes(forLesson?.lessons); + // } + + setCoursesInfo(forLesson || null); + } + }, [courses]); + + useEffect(() => { + const check = answer?.find((item) => item?.is_correct); + if (check) { + setAnswerCheck(true); + } else { + setAnswerCheck(false); + } + }, [answer]); + + useEffect(() => { + if (test?.answer_id && test?.answer_id != null) { + setSelectedAnswer(true); + } else { + setSelectedAnswer(false); + } + }, [test]); + + useEffect(() => { + if (lessonNavigation) { + for (let i = 0; i < lessonNavigation?.lessons?.length; i++) { + const el: lessonType = lessonNavigation?.lessons[i]; + if (el?.id === lessonNavigation?.lesson?.id) { + if (lessonNavigation?.lessons[i + 1]) { + setNextLesson(lessonNavigation.lessons[i + 1]); + } else { + setNextLesson(null); + } + + if (lessonNavigation?.lessons[i - 1]) { + setPrevLesson(lessonNavigation.lessons[i - 1]); + } else { + setPrevLesson(null); + } + } + } + } + }, [lessonNavigation]); + + useEffect(() => { + handleFetchLessons(); + handleStep(); + + if (contextNotificationId && contextNotificationId != null) { + handleStatusView(contextNotificationId); + } + + const stepSend = async () => { + if (lesson_id && stream_id) { + const data = await handleMainLesson(Number(lesson_id), Number(stream_id)); // steps + // console.log(data); + if (data) { + const validSteps = data?.filter((item: StepApi) => item?.id_parent); + for (let i = 0; i < validSteps?.length; i++) { + const step: StepApi = validSteps[i]; + if (step.id_parent && step?.id === Number(id)) { + if (validSteps) { + const stepsLength = validSteps.length; + const currentStepPos = i + 1; + const nextStep = validSteps[i + 1] || null; + const prevStep = validSteps[i - 1] || null; + + setStepNavigation((prev) => { + // Если prev — null, создаем новое состояние. + // Если prev существует, используем его для объединения. + return { + ...(prev || {}), // Используем существующее или пустой объект + stepsLength, + currentStepPos, + nextStep, + prevStep + }; + }); + } + } + } + } + } + }; + stepSend(); + }, []); + + const docSection = ( +
+
+
+ {steps?.type?.title} + +
+
+ {document?.content?.title} + {document?.content?.description &&
{document?.content?.description &&
{document?.content?.description}
}
} +
+
+ {/* +
+
+ +
+ ); + + const linkSection = ( +
+
+
+ {steps?.type?.title} + +
+
+ {link?.content?.title} + {link?.content?.description &&
{link?.content?.description &&
{link?.content?.description}
}
} +
+
+ {translations.linkLabel}: + + {link?.content?.url} + +
+
+
+ ); + + const practicaSection = ( +
+
+
+ {steps?.type?.title} + +
+
+ {translations.score}: + {`${steps?.score}`} +
+
+ +
+ {practica?.content?.title} + +
+ {practica?.content?.description &&
} + +
+
+ {practica?.content?.document_path && practica?.content.document_path.toLowerCase().includes('pdf') && ( + <> + Документ: + + + )} +
+ +
+ {practica?.content?.url && ( +
+ {translations.linkLabel}: + {practica?.content.url && ( + + {practica?.content.url} + + )} +
+ )} +
+
+
+
+ + {steps?.chills ? ( + <> + {translations.taskCompleted} + + ) : ( +
+ {translations.taskInstruction} +
+ { + const file = e.target.files?.[0]; + if (file) { + const maxSize = 10 * 1024 * 1024; + + if (file.size > maxSize) { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.fileTooLarge, detail: translations.maxFileSize10mb } + }); + } else { + setDocValue((prev) => ({ + ...prev, + file: file + })); + } + } + }} + /> +
+
+ {progressSpinner && } +
+
+ )} + +
+ ); + + const testSection = ( +
+ {progressSpinner && ( +
+ +
+ )} +
+
+
+ {steps?.type?.title} + +
+
+
+ {translations.score}: + {`${steps?.score}`} +
+
+
+
+ {test?.content?.content} +
+ {test?.content?.answers.map((item, index) => { + return ( +
+ {selectedAnswer ? ( + <> + +
{item.text}
+ + ) : ( + <> + +
{item.text}
+ + )} +
+ ); + })} +
+
+
+ + {steps?.count_attempt && steps?.count_attempt >= 3 ? ( + {translations.taskCompleted} + ) : ( +
+
+ )} + + {/* История ответов */} + {steps?.details && steps.details.length > 0 && ( +
+

{translations.answerHistory}

+
+ {(steps.details as any[]).map((attempt, index) => ( +
+
+
+ + {translations.attempt} #{index + 1} + + {attempt.is_correct ? ( + + + {translations.correct} + + ) : ( + + + {translations.incorrect} + + )} +
+ {attempt?.answers?.text ?
+ Вариант: + {attempt.answers.text} +
: ''} +
+
+ ))} +
+
+ )} + {steps?.my_score != null && ( +
+ {translations.yourTotalScore}: + {`${steps.my_score}`} +
+ )} +
+ ); + + const videoSection = ( +
+
+
+ {steps?.type?.title} + +
+
+ {video?.content?.description && ( +
+ {video?.content?.title} + {video?.content?.description &&
{video?.content?.description &&
{video?.content?.description}
}
} +
+ )} +
+
+ {preview ? ( +
+
+ +
+ Видео +
+ ) : ( + + )} +
+
+
+ ); + + if (mainProgressSpinner) + return ( +
+ +
+ ); + + return ( +
+
+ {/* step navigation */} + {stepNavigation && stepNavigation.currentStepPos ? ( +
+ {stepNavigation?.prevStep && ( + { + router.push(`/teaching/lessonView/${lesson_id}/${subject_id}/${stream_id}/${stepNavigation?.prevStep?.id}/${id_edu_year}`); + }} + className="pi pi-angle-left text-bold cursor-pointer p-1 rounded-full hover:bg-[var(--mainColor)] hover:text-white" + > + )} +
+ {translations.step} + {stepNavigation?.currentStepPos} +
+
+ {translations.from} + {stepNavigation?.stepsLength} +
+ {stepNavigation?.nextStep && ( + { + router.push(`/teaching/lessonView/${lesson_id}/${subject_id}/${stream_id}/${stepNavigation?.nextStep?.id}/${id_edu_year}`); + }} + className="pi pi-angle-right text-bold cursor-pointer p-1 rounded-full hover:bg-[var(--mainColor)] hover:text-white" + > + )} +
+ ) : ( + '' + )} + + {/* course info */} +
+ {/*{contextLastSubjectPageVisit ? (*/} + {/* setContextLastSubjectPageVisit(null)} href={`/teaching/${contextLastSubjectPageVisit}`}>*/} + {/* */} + {/* */} + {/*) : (*/} + {/* ''*/} + {/*)}*/} +
+
0 ? 'justify-around flex-col sm:flex-row' : 'justify-center'} items-center`}> +
+

+ {getLocalized(courseInfo, 'title') || courseInfo?.title} +

+
+

{translations.theme}:

+

{lessonName ? lessonName : '------'}

+
+ {getLocalized(courseInfo, 'description') || courseInfo?.description} +
+ {courseInfo?.image && courseInfo?.image.length > 0 && ( +
+ +
+ )} +
+
+
+ + {/* main */} + {hasSteps && } + {type === 'document' && docSection} + {type === 'link' && linkSection} + {type === 'practical' && practicaSection} + {type === 'test' && testSection} + {type === 'video' && videoSection} +
+ + {/* lesson navigation */} +
+
+ {prevLesson?.title && ( + + )} + {nextLesson?.title && ( + <> + {translations.theme} + + + )} +
+
+
+ ); +} diff --git a/app/(student)/teaching/page.tsx b/app/(student)/teaching/page.tsx new file mode 100644 index 00000000..ddfd8bca --- /dev/null +++ b/app/(student)/teaching/page.tsx @@ -0,0 +1,253 @@ +'use client'; + +import ItemCard from '@/app/components/cards/ItemCard'; +import { NotFound } from '@/app/components/NotFound'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { fetchItemsConnect, fetchItemsLessons, studentEduYear } from '@/services/studentMain'; +import Link from 'next/link'; +import { Dropdown } from 'primereact/dropdown'; +import { ReactElement, useContext, useEffect, useState } from 'react'; +import { ProgressSpinner } from 'primereact/progressspinner'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import { useLocalizedData } from '@/hooks/useLocalizedData'; +import MainTitle from '@/app/components/titles/MainTitle'; + +export default function Teaching() { + interface sortOptType { + name: string; + code: number; + } + + interface EduYearType { + id: number; + name_ru: string; + } + + const [lessons, setLessons] = useState>({ + 1: { semester: { name_kg: '' } } + }); + + const { translations } = useLocalization(); + const { getLocalized } = useLocalizedData(); + + const [lessonsDisplay, setLessonsDisplay] = useState([]); + const [hasLessons, setHasLessons] = useState(false); + const [selectedSort, setSelectedSort] = useState({ name: translations.all, code: 0 }); + const [sortOpt, setSortOpt] = useState(); + const [connection, setConnection] = useState<[]>([]); + const [skeleton, setSkeleton] = useState(false); + const [mainProgressSpinner, setMainProgressSpinner] = useState(false); + + const [eduYearOpt, setEduYearOpt] = useState([]); + const [eduYearSelected, setEduYearSelected] = useState(); + + const { setMessage } = useContext(LayoutContext); + const showError = useErrorMessage(); + + // functions + const toggleSortSelect = (e: sortOptType) => { + setSelectedSort(e); + }; + + const toggleEduYearSelect = (e: EduYearType) => { + setEduYearSelected(e); + }; + + const toggleSkeleton = () => { + setSkeleton(true); + setTimeout(() => { + setSkeleton(false); + }, 1000); + }; + + // fetch lessons + const handleFetchLessons = async (eduYear: number) => { + setSkeleton(true); + setMainProgressSpinner(true); + console.log(eduYear); + const data = await fetchItemsLessons(eduYear); + if (data && data?.success) { + // валидность проверить + setLessons(data.data); + setHasLessons(false); + } else { + setHasLessons(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: translations.tryAgainLater } + }); + if (data?.data?.response?.status) { + showError(data?.data.response.status); + } + } + setSkeleton(false); + setMainProgressSpinner(false); + }; + + const handleFetchConnectId = async () => { + const data = await fetchItemsConnect(); + toggleSkeleton(); + if (data) { + setConnection(data); + } else { + setHasLessons(true); + setMessage({ + state: true, + value: { severity: 'error', summary: translations.error, detail: '' } + }); + if (data?.response?.status) { + showError(data.response.status); + } + } + }; + + const formatEduYear = (): string => { + const now = new Date(); + const month = now.getMonth() + 1; // 1-12 + const year = now.getFullYear(); + + // С сентября (9) по декабрь — новый учебный год начался + // С января по август — всё ещё предыдущий учебный год + const startYear = month >= 9 ? year : year - 1; + const endYear = String(startYear + 1).slice(-2); + + return `${startYear}-${endYear}`; + }; + + const handleFetchEduYear = async ()=> { + const data = await studentEduYear(); + if (data) { + setEduYearOpt(data); + + const date = formatEduYear(); + const currentDate = data?.find((item: EduYearType)=> item?.name_ru === date); + if(currentDate){ + setEduYearSelected(currentDate); + } + } + } + + useEffect(() => { + if (!lessons) return; + + // готовим опции для dropdown + let forSortSelect = [{ name: translations.all, code: 0 }]; + + Object.entries(lessons).forEach(([key, value]) => { + if (value.semester) { + forSortSelect.push({ + name: getLocalized(value.semester, 'name') || value.semester.name_kg, + code: Number(key) + }); + } + }); + + setSortOpt(forSortSelect); + + // фильтрация по selectedSort + let displayData; + if (selectedSort?.code === 0) { + displayData = Object.values(lessons).filter((item: any) => item.semester); + } else { + const selected = lessons[selectedSort.code]; + displayData = selected && selected.semester ? [selected] : []; + } + + if(displayData?.length < 1){ + setHasLessons(true); + } + + // превращаем в jsx + const x = displayData.map((semester: any, sIdx: number) => ( +
+

{getLocalized(semester.semester, 'name') || semester.semester.name_kg}

+
+ {Object.values(semester) + .filter((val: any) => val.subject) + .map((subj: any, subjIdx: number) => { + return subj.connect ? ( + + + + ) : ( + + + + ); + })} +
+
+ )); + + setLessonsDisplay(x); + }, [lessons, selectedSort, translations]); + + useEffect(() => { + handleFetchConnectId(); + handleFetchEduYear(); + }, []); + + useEffect(()=> { + if(eduYearSelected?.id){ + handleFetchLessons(eduYearSelected.id); + } + },[eduYearSelected]); + + // Update default values when language changes + useEffect(() => { + if (selectedSort.code === 0) { + setSelectedSort(prev => ({ ...prev, name: translations.all })); + } + }, [translations]); + + return ( + <> +
+
+ {/* info section */} + {/*{skeleton ? (*/} + {/* */} + {/*) : (*/} +
+

{translations.trainingPlan}

+
+ { + toggleEduYearSelect(e.value); + }} + options={eduYearOpt} + optionLabel="name_ru" + className="w-full sm:w-14rem p-inputtext-sm" + /> + + { + toggleSortSelect(e.value); + }} + options={sortOpt} + optionLabel="name" + className="w-full sm:w-14rem p-inputtext-sm" + /> +
+
+ {/*// )}*/} + + {/* lesson section */} + {!mainProgressSpinner ? + hasLessons ? : + skeleton ? + + :
{lessonsDisplay}
+ :
+ +
+ } +
+
+ + ); +} diff --git a/app/components/BaseLayout.tsx b/app/components/BaseLayout.tsx new file mode 100644 index 00000000..8095e684 --- /dev/null +++ b/app/components/BaseLayout.tsx @@ -0,0 +1,19 @@ +'use client'; + +import AppTopbar from '@/layout/AppTopbar'; +import HomeClient from './HomeClient'; +import AppFooter from '@/layout/AppFooter'; + +export default function BaseLayout() { + return ( + <> +
+ +
+ +
+ +
+ + ); +} diff --git a/app/components/CKEditorWrapper.tsx.tsx b/app/components/CKEditorWrapper.tsx.tsx new file mode 100644 index 00000000..717d55a4 --- /dev/null +++ b/app/components/CKEditorWrapper.tsx.tsx @@ -0,0 +1,49 @@ +'use client'; +import useTypingEffect from '@/hooks/useTypingEffect'; +import { Editor } from 'primereact/editor'; +import { useEffect, useState } from 'react'; +import { EditorTextChangeEvent } from 'primereact/editor'; + +export default function CKEditorWrapper({ textValue, insideValue }: { textValue: (e: string) => void; insideValue: string }) { + const [text, setText] = useState(''); + const [toggleTyping, setToggleTyping] = useState(true); + + // const typedText = useTypingEffect( + // 'Текстти ушул жерге жазыныз', + // toggleTyping + // ); + + useEffect(() => { + // const parser = new DOMParser(); + // const doc = parser.parseFromString(text, 'text/html'); + // const imgs = doc.querySelectorAll('img'); + // console.log(imgs); + + // for (let img of imgs) { + // console.log(img.src.startsWith('data:image')); + + // // if (img.src.startsWith('data:image')) { + // // const res = await fetch('/api/upload', { + // // method: 'POST', + // // headers: { 'Content-Type': 'application/json' }, + // // body: JSON.stringify({ file: img.src }) + // // }); + + // // const data = await res.json(); + // // img.src = data.url; + // // } + // } + + if (text) textValue(text); + }, [text]); + + return ( +
+ {insideValue ? ( + setText(e.htmlValue)} className="w-[800px] h-[300px]" /> + ) : ( + setText(e.htmlValue)} className="w-[800px] h-[300px]" /> + )} +
+ ); +} diff --git a/app/components/Contribution.tsx b/app/components/Contribution.tsx new file mode 100644 index 00000000..ff8b9cd2 --- /dev/null +++ b/app/components/Contribution.tsx @@ -0,0 +1,67 @@ +'use client'; +import React, { useEffect, useRef } from 'react'; +import HeatMap from '@uiw/react-heat-map'; // <-- Новый импорт! +import { ContributionDay } from '@/types/ContributionDay'; +import useMediaQuery from '@/hooks/useMediaQuery'; + +// Интерфейс для данных остается прежним (но у HeatMap используется prop 'value') + +const ActivityHeatmap = ({ value }: { value: ContributionDay[] | null }) => { + const ref = useRef(null); + const media = useMediaQuery('(max-width: 640px)'); + + const months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек']; + const weekdays = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб']; + + // const start = new Date('2025-01-01'); // 1 января + // const end = new Date('2025-12-31'); // 31 декабря + + const end = new Date(); // сегодня + const start = new Date(); + start.setFullYear(end.getFullYear() - 1); + + useEffect(() => { + if (media) { + if (ref.current) { + ref.current.scrollLeft = ref.current.scrollWidth; + } + } + }, [media]); + + return ( +
+ {value && value?.length && ( + { + // data = { date, count, column, row, index } + return ( + + {`${data?.date && String(data.date ?? '—')}: ${data?.count || 0} активностей`} + + ); + }} + monthLabels={months} + weekLabels={weekdays} + className="w-full min-w-[900px] m-auto flex " + // onClick в этой библиотеке называется rectRender или нужно использовать обертку + // Здесь мы его пока опустим, чтобы сфокусироваться на отображении + /> + )} +
+ ); +}; + +export default ActivityHeatmap; diff --git a/app/components/CounterBanner.tsx b/app/components/CounterBanner.tsx new file mode 100644 index 00000000..db9e3ff8 --- /dev/null +++ b/app/components/CounterBanner.tsx @@ -0,0 +1,70 @@ +import React, { useEffect, useState } from 'react'; +import { faCircle, faChalkboard, faUserGraduate, faBookOpen, faShieldHeart } from '@fortawesome/free-solid-svg-icons'; +import MyFontAwesome from './MyFontAwesome'; +import CountUp from 'react-countup'; +import { MainPageStatistics } from '@/types/main/MainPageStatistic'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function CounterBanner({statisticValue}: {statisticValue: MainPageStatistics | null}) { + const { translations } = useLocalization(); + + const [statistics, setStatistics] = useState(null); + + useEffect(()=> { + if(statisticValue){ + setStatistics(statisticValue); + } else { + setStatistics(null); + } + },[statisticValue]); + + return ( +
+
+
+
+ + +
+
+
+
+ {translations.coursesAndVideoLessons} +
+
+ +
+
+ + +
+
+
+
+ {translations.registeredStudents} +
+
+ +
+
+ + +
+
+
+
+ {translations.teachers} +
+
+ +
+
+ + +
+
+
%
+ {translations.satisfactionLevel} +
+
+
+
+ ); +} diff --git a/app/components/HomeClient.tsx b/app/components/HomeClient.tsx new file mode 100644 index 00000000..391647ff --- /dev/null +++ b/app/components/HomeClient.tsx @@ -0,0 +1,435 @@ +'use client'; + +import AOS from 'aos'; +import 'aos/dist/aos.css'; +import { useContext, useEffect, useState } from 'react'; +import CounterBanner from './CounterBanner'; +import Link from 'next/link'; +import VideoPlay from './VideoPlay'; +import FancyLinkBtn from './buttons/FancyLinkBtn'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import useMediaQuery from '@/hooks/useMediaQuery'; +import { MainPageStatistics } from '@/types/main/MainPageStatistic'; +import { mainPageStatistics } from '@/services/main/main'; +import useErrorMessage from '@/hooks/useErrorMessage'; +import { fetchOpenCoursesMainPage, openCourseShow, openCourseSignup, signupList } from '@/services/openCourse'; +import { myMainCourseType } from '@/types/myMainCourseType'; +import MyDateTime from './MyDateTime'; +import { OptionsType } from '@/types/OptionsType'; +import GroupSkeleton from './skeleton/GroupSkeleton'; +import { Sidebar } from 'primereact/sidebar'; +import OpenCourseShowCard from './cards/OpenCourseShowCard'; +import { CourseCategoryOption } from '@/types/openCourse/CourseCategoryOption'; +import { useLocalization } from '@/layout/context/localizationcontext'; +import AppConfig from '@/layout/AppConfig'; +import { getToken } from '@/utils/auth'; + +export default function HomeClient() { + // types + + const params = new URLSearchParams(); + const { translations } = useLocalization(); + const { user, setGlobalLoading, setMessage } = useContext(LayoutContext); + const showError = useErrorMessage(); + + const [statistics, setStatistics] = useState(null); + const [skeleton, setSkeleton] = useState(false); + const [showVisisble, setShowVisible] = useState(false); + const [hasCourses, setHasCourses] = useState(false); + const [coursesValue, setValueCourses] = useState([]); + const [courseDetail, setCourseDetail] = useState(null); + const [signUpList, setSignupList] = useState([]); + const [newCourses, setNewCourses] = useState([]); + const [bestCourses, setBestCourses] = useState([]); + const [popularCourses, setPopular_courses] = useState([]); + + const options: OptionsType = { + year: '2-digit', + month: 'short', // 'long', 'short', 'numeric' + day: '2-digit', + // hour: '2-digit', + // minute: '2-digit', + hour12: false // 24-часовой формат + }; + const media = useMediaQuery('(max-width: 640px)'); + + const handleMainPageStatistics = async () => { + const data = await mainPageStatistics(); + if ((data && data?.students) || data?.workers || data?.course) setStatistics(data); + }; + + const handleFetchNewCourse = async () => { + setSkeleton(true); + const data = await fetchOpenCoursesMainPage(); + if (data && data?.new_courses) { + setHasCourses(false); + MainCoursesPreperation(data); + setNewCourses(data?.new_courses); + setBestCourses(data?.best_courses); + setPopular_courses(data?.popular_courses); + } + setSkeleton(false); + }; + + const MainCoursesPreperation = (data: Record)=> { + const forMainCourses = []; + for (const key in data) { + const coureseTypeItems = data[key]; + forMainCourses.push(...coureseTypeItems); + } + if(forMainCourses?.length){ + setValueCourses(forMainCourses); + } + } + + const imageBodyTemplate = (product: any, idx: number) => { + const image = product.image; + + if (typeof image === 'string') { + return ( +
+ Course image +
+ ); + } + + return ( + //
+
+ Course image + {/*
*/} +
+ ); + }; + + const OpenCourse = ({ course, index }: { course: CourseCategoryOption; index: number }) => { + return ( +
+
+
{imageBodyTemplate(course, index)}
+
+ + {course?.audience_type?.name === 'open' ? 'Бесплатный' : course?.audience_type?.name === 'wallet' ? 'Платный' : ''} +
+
+ +
+ {course.status ? ( + handleCourseShow(course?.id)} + className="cursor-pointer w-full sm:max-w-[350px] break-words text-[var(--mainColor)] underline underline-offset-4 decoration-[var(--mainColor)]/40 hover:decoration-[var(--mainColor)]" + > + {course?.title} + + ) : ( + + {course?.title} + + )} + +
+ {course?.description} +
+
+ +
+ { + course?.category?.title ? +
+

{course?.category.title}

+
+ : '' + } + + { + course?.is_featured ? + + : '' + } +
+ +
+
+ {/* Автор: */} +
+ {!media ? ( + <> + {course?.user?.last_name} + {course?.user?.name} + {course?.user?.father_name} + + ) : ( + <> + {course?.user?.last_name} + {course?.user?.name[0]}. + {course?.user?.father_name && course?.user?.father_name[0] + '.'} + + )} +
+
+ + {/* data */} +
+ +
+
+
+ ); + }; + + const handleCourseShow = async (course_id: number) => { + setShowVisible(true); + setSkeleton(true); + const data = await openCourseShow(course_id); + + if (data && Object.values(data)?.length) { + setCourseDetail(data); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: 'Повторите позже' } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + setSkeleton(false); + }; + + // signUp + const сourseSignup = async (course_id: number) => { + const data = await openCourseSignup(course_id); + if (data?.success) { + handleSendSingup(); + setMessage({ + state: true, + value: { severity: 'success', summary: 'Успешное добавление!', detail: '' } + }); + } else { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: translations.tryAgainLater } + }); + if (data?.response?.status) { + if (data?.response?.status == '400') { + setMessage({ + state: true, + value: { severity: 'error', summary: translations.errorTitle, detail: data?.response?.data?.message } + }); + } else { + showError(data.response.status); + } + } + } + }; + + // signup courses list + const handleSignupList = async (course: any) => { + course?.forEach((i: { id: number }) => params.append('course_Ids[]', String(i?.id))); + const data = await signupList(params); + if (data && data?.signed_courses) { + return data?.signed_courses; + } else { + return null; + } + }; + + const handleSendSingup = async () => { + const token = getToken('access_token'); + if(token){ + const list: any | null = await handleSignupList(coursesValue); + if (list) { + setSignupList(list); + } + } + }; + + useEffect(() => { + if (coursesValue?.length) { + handleSendSingup(); + } + }, [coursesValue]); + + useEffect(() => { + handleFetchNewCourse(); + // handleFetchOpenCourse(pageState, '', ''); + setGlobalLoading(true); + handleMainPageStatistics(); + setTimeout(() => { + setGlobalLoading(false); + }, 800); + AOS.init(); + }, []); + + return ( + <> +
+
+ {/* {"Уроки"} */} +
+
+
+
+
+ Фото + + {translations.convenientOnlineLearningSpace} + +
+

+ {translations.welcomeToDistanceLearningPortal} +

+
+ {translations.weUniteUniversityProjects} +
    +
  • {translations.openOnlineHome}
  • +
  • {translations.higherEducationPrograms}
  • +
+ {user ? ( + + + + ) : media ? ( + + + + ) : ( + '' + )} +
+
+
+
+
+ + {/* Counter Statistics */} + + + + + {/* open courses */} + +
+ {newCourses?.length > 0 ? ( + skeleton ? ( +
+ + + + + +
+ ) : ( +
+

{translations.newCourses}

+
+ {newCourses?.map((item, idx) => { + return ; + })} +
+
+ ) + ) : ( + '' + )} + + {popularCourses?.length > 0 ? ( + skeleton ? ( +
+ + + + + +
+ ) : ( +
+

{translations.popularCourses}

+
+ {popularCourses?.map((item, idx) => { + return ; + })} +
+
+ ) + ) : ( + '' + )} + + {bestCourses?.length > 0 ? ( + skeleton ? ( +
+ + + + + +
+ ) : ( +
+

{translations.recommendedByDepartment}

+
+ {bestCourses?.map((item, idx) => { + return ; + })} +
+
+ ) + ) : ( + '' + )} + + {newCourses?.length || bestCourses?.length || popularCourses?.length ? ( + + {translations.allOpenCourses} + + + ) : ( + '' + )} +
+ + setShowVisible(false)}> + {skeleton ? ( + + ) : courseDetail ? ( + + ) : ( + {translations.dataNotAvailable} + )} + + + {/* Oshgu Video */} +
+

+ {translations.videoTourMainBuilding} {translations.oshSU} +

+
+ +
+ + ); +} + + + diff --git a/app/components/InfoBanner.tsx b/app/components/InfoBanner.tsx new file mode 100644 index 00000000..2ad272af --- /dev/null +++ b/app/components/InfoBanner.tsx @@ -0,0 +1,13 @@ +'use client'; + +import useMediaQuery from '@/hooks/useMediaQuery'; + +export default function InfoBanner({ title, titleSize }: { title: string; titleSize: { default: string; sm: string } }) { + const media = useMediaQuery('(max-width: 640px)'); + + return ( +
+

{title}

+
+ ); +} diff --git a/app/components/LocalizationSwift.tsx b/app/components/LocalizationSwift.tsx new file mode 100644 index 00000000..c28648f8 --- /dev/null +++ b/app/components/LocalizationSwift.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { useLocalization } from '@/layout/context/localizationcontext'; + +export default function LocalizationSwift( ) { + const { language, setLanguage } = useLocalization(); + + return
setLanguage(language === 'ru' ? 'ky' : 'ru')}> + + {language.toUpperCase()} +
+} diff --git a/app/components/MyDateTime.tsx b/app/components/MyDateTime.tsx new file mode 100644 index 00000000..d78c93ab --- /dev/null +++ b/app/components/MyDateTime.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { OptionsType } from "@/types/OptionsType"; +import { useEffect, useState } from "react"; + +export default function MyDateTime({createdAt, options}: {createdAt: string | Date | null, options: OptionsType}){ + const [result, setResult] = useState(''); + + useEffect(()=> { + if (createdAt) { + const dateObject = new Date(createdAt); + if (dateObject) { + const formattedString = dateObject.toLocaleString('ru-RU', options); + const forResult = formattedString?.replace(/,/g, ''); + if (formattedString) { + setResult(forResult); + } else { + return setResult('----'); + } + } else { + return setResult('----'); + } + } else { + return setResult('----'); + } + },[createdAt]); + + return ( + {result} + ); +} diff --git a/app/components/MyFontAwesome.tsx b/app/components/MyFontAwesome.tsx new file mode 100644 index 00000000..28e58167 --- /dev/null +++ b/app/components/MyFontAwesome.tsx @@ -0,0 +1,17 @@ +"use client"; // обязательно в app/ структуре + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { ComponentProps } from "react"; + +interface IconProps extends ComponentProps { + icon: IconProp; + className?: string; + size?: "xs" | "lg" | "sm" | "1x" | "2x" | "3x" | "4x" | "5x" | "6x" | "7x" | "8x" | "9x" | "10x"; +} + +export default function MyFontAwesome({ icon, className, size, ...props }:IconProps ) { + return ( + + ); +} \ No newline at end of file diff --git a/app/components/NotFound.tsx b/app/components/NotFound.tsx new file mode 100644 index 00000000..c43b5684 --- /dev/null +++ b/app/components/NotFound.tsx @@ -0,0 +1,19 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { useLocalization } from '@/layout/context/localizationcontext'; + +export const NotFound = ({ titleMessage }: { titleMessage: string }) => { + const { translations } = useLocalization(); + return ( +
+
+

{titleMessage}

+ + {translations?.backHome} + +
+
+ ); +}; diff --git a/app/components/SessionManager.tsx b/app/components/SessionManager.tsx new file mode 100644 index 00000000..54e65384 --- /dev/null +++ b/app/components/SessionManager.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { getToken } from '@/utils/auth'; +import { useContext, useEffect, useState } from 'react'; +import { LayoutContext } from '@/layout/context/layoutcontext'; +import { getUser } from '@/services/auth'; +import { logout } from '@/utils/logout'; +import { usePathname } from 'next/navigation'; + +const SessionManager = () => { + const { user, setMessage, setGlobalLoading, setUser, departament, setDepartament } = useContext(LayoutContext); + + const pathname = usePathname(); + + useEffect(() => { + const init = async () => { + console.log('проверяем токен...'); + const token = getToken('access_token'); + if (token) { + const res = await getUser(); + setGlobalLoading(true); + try { + if (res?.success) { + setTimeout(() => { + setGlobalLoading(false); + }, 1000); + // console.log('Данные пользователя успешно пришли ', res); + if (res.roles && res.roles.length > 0) { + const roleCheck = res.roles.find((i: { id_role: number }) => i.id_role); + if (roleCheck) { + setDepartament({ info: roleCheck.roles_name.info_ru, last_name: res.user?.last_name, name: res?.user.name, father_name: res.user?.father_name }); + } + } + const userVisit = localStorage.getItem('userVisit'); + if (!userVisit) { + localStorage.setItem('userVisit', JSON.stringify(true)); + + setMessage({ + state: true, + value: { severity: 'success', summary: 'Успешная авторизация!', detail: '' } + }); // messege - Успех! + } + setUser(res.user); + } else { + // logout({ setUser, setGlobalLoading }); + setGlobalLoading(false); + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка', detail: 'Ошибка при авторизации' } + }); // messege - Ошибка при авторизации + console.log('Ошибка при получении пользователя'); + } + } catch (error) { + // logout({ setUser, setGlobalLoading }); + setGlobalLoading(false); + setMessage({ + state: true, + value: { severity: 'error', summary: 'Ошибка', detail: 'Ошибка при авторизации' } + }); // messege - Ошибка при авторизации + console.log('Ошибка при получении пользователя'); + } + setGlobalLoading(false); + } + }; + init(); + }, []); + + useEffect(() => { + if (!pathname.startsWith('/teaching/lesson/') && !pathname.startsWith('/course/')) { + // setGlobalLoading(true); + } + + const token = getToken('access_token'); + + if (!token && pathname !== '/' && pathname !== '/auth/login') { + console.log('Перенеправляю в login'); + + logout({ setUser, setGlobalLoading }); + window.location.href = '/auth/login'; + return; + } + }, [pathname]); + + return null; +}; + +export default SessionManager; diff --git a/app/components/TeacherEditor.tsx b/app/components/TeacherEditor.tsx new file mode 100644 index 00000000..cc36c8fc --- /dev/null +++ b/app/components/TeacherEditor.tsx @@ -0,0 +1,280 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkMath from 'remark-math'; +import rehypeKatex from 'rehype-katex'; +import { Button } from 'primereact/button'; +import { Dialog } from 'primereact/dialog'; // Import Dialog +import { PrimeIcons } from 'primereact/api'; +import GroupSkeleton from '@/app/components/skeleton/GroupSkeleton'; +import LessonCard from '@/app/components/cards/LessonCard'; +import { InputText } from 'primereact/inputtext'; +import { faSquareRootVariable } from '@fortawesome/free-solid-svg-icons'; // Import PrimeIcons + +export default function TeacherEditor({state, onSave, defaultValueProp}: {state: boolean, onSave: (text: string) => void, defaultValueProp: string | null}) { + const MATH_TEMPLATES = [ + { label: 'Формула в тексте', before: '$', after: '$' }, + { label: 'Блок формулы', before: '$$\n', after: '\n$$' }, + { label: 'Дробь', before: '\\frac{', after: '}{}' }, + { label: 'Корень √', before: '\\sqrt{', after: '}' }, + { label: 'Корень ³√', before: '\\sqrt[3]{', after: '}' }, + { label: 'Степень x²', before: '^', after: '' }, + { label: 'Не равно (≠)', before: '\\neq ', after: '' }, + { label: 'Приближенно (≈)', before: '\\approx ', after: '' }, + ]; + + const parseText = (html: any) => { + const doc = new DOMParser().parseFromString(html, 'text/html'); + return doc.body.textContent || ""; + }; + + const defaultValue = defaultValueProp ? parseText(defaultValueProp) : '### Пример задание №1\nНайдите корни уравнения: $$x^2 - 5x + 6 = 0$$\n\n*Подсказка: используйте формулу дискриминанта $D = b^2 - 4ac$.*'; + + // Начальный текст-пример для преподавателя + const [text, setText] = useState(defaultValue); + const [isSaving, setIsSaving] = useState(false); + const [displayDialog, setDisplayDialog] = useState(false); // State for Dialog visibility + const textareaRef = useRef(null); + + // Функция для быстрой вставки формул по кнопкам + const insertTemplate = (before: string, after: string) => { + const textarea = textareaRef.current; + if (!textarea) return; + + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const currentText = textarea.value; + + const selectedText = currentText.substring(start, end); + const replacement = before + selectedText + after; + + const newText = currentText.substring(0, start) + replacement + currentText.substring(end); + + setText(newText); + + // Возвращаем фокус на поле ввода + setTimeout(() => { + textarea.focus(); + textarea.setSelectionRange(start + before.length, start + before.length + selectedText.length); + }, 10); + }; + + const katex_info = ( +
+
+

+ Правила написания формул +

+ +
    +
  • + Формулы писать только внутри $...$ или $$...$$ +
  • + +
  • + Обычный текст и определения писать вне знаков доллара +
  • + +
  • + Каждую формулу писать отдельно +
  • + +
  • + Нельзя писать несколько формул внутри одного $...$ +
  • +
+
+ +
+

+ Примеры +

+ +
+
+ Приблизительно равно + +
+ + $x \approx y$ + + +
+ x≈y +
+
+
+ +
+ Дробь + +
+ + $\frac{"{3}"}{"{2}"}$ + + +
+ 3/2 +
+
+
+ +
+ Квадратный корень + +
+ + $\sqrt{"{3}"}$ + + +
+ √3 +
+
+
+ +
+ Корень с индексом + +
+ + $\sqrt[2]{"{4}"}$ + + +
+ √4 +
+
+
+ +
+ Степень + +
+ + $x^3$ + + +
+ x³ +
+
+
+ +
+ Не равно + +
+ + $4 \neq 5$ + + +
+ 4≠5 +
+
+
+
+
+ +
+ + Неправильно + + + + $x^2 y^2 \frac{"{1}"}{"{2}"}$ + + +

+ Несколько формул внутри одного блока +

+
+ +
+ + Правильно + + + + $x^2$ $y^2$ $\frac{"{1}"}{"{2}"}$ + + +

+ Каждая формула отдельно +

+
+
+ ); + + useEffect(() => { + if(!state) { + onSave(text); + } + }, [text]); + + return ( +
+ {/* Панель быстрых кнопок-шаблонов */} +
+
{/* Flex container for icon and span */} + setDisplayDialog(true)} + > + Формулы: +
+
+ {MATH_TEMPLATES.map((tmpl, idx) => ( + + ))} +
+
+ + {/* Две колонки: Редактор и Предпросмотр */} +
+ {/* Левая колонка: Ввод */} +
+