Обновлены компоненты для учета только прошедших лекций в статистике посещаемости. Добавлено мобильное отображение в компонентах LessonItems и Item, улучшена логика фильтрации лекций. Реализовано отображение QR-кода с учетом темы оформления.
This commit is contained in:
parent
d13bff5331
commit
1b337278fe
@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { AttendanceData } from './useAttendanceData'
|
||||
|
||||
export interface AttendanceStats {
|
||||
@ -27,13 +28,17 @@ export const useAttendanceStats = (data: AttendanceData): AttendanceStats => {
|
||||
}
|
||||
}
|
||||
|
||||
const totalLessons = data.attendance.length
|
||||
const now = dayjs()
|
||||
// Фильтруем лекции, оставляя только те, которые уже прошли (исключаем будущие)
|
||||
const pastLessons = data.attendance.filter(lesson => dayjs(lesson.date).isBefore(now))
|
||||
|
||||
const totalLessons = pastLessons.length
|
||||
|
||||
// Рассчитываем посещаемость для каждого студента
|
||||
const studentAttendance = data.students.map(student => {
|
||||
let attended = 0
|
||||
|
||||
data.attendance.forEach(lesson => {
|
||||
pastLessons.forEach(lesson => {
|
||||
if (lesson.students.some(s => s.sub === student.sub)) {
|
||||
attended++
|
||||
}
|
||||
@ -48,7 +53,7 @@ export const useAttendanceStats = (data: AttendanceData): AttendanceStats => {
|
||||
})
|
||||
|
||||
// Рассчитываем статистику посещаемости для каждого урока
|
||||
const lessonsAttendance = data.attendance.map(lesson => {
|
||||
const lessonsAttendance = pastLessons.map(lesson => {
|
||||
const attendedStudents = lesson.students.length
|
||||
const attendancePercent = data.students.length > 0
|
||||
? (attendedStudents / data.students.length) * 100
|
||||
|
@ -126,9 +126,21 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
}
|
||||
|
||||
const studentsMap = new Map()
|
||||
const now = dayjs()
|
||||
|
||||
// Собираем данные о всех студентах
|
||||
lessonList.forEach(lesson => {
|
||||
// Фильтруем только прошедшие лекции
|
||||
const pastLessons = lessonList.filter(lesson => dayjs(lesson.date).isBefore(now))
|
||||
|
||||
// Если прошедших лекций нет, возвращаем пустую статистику
|
||||
if (pastLessons.length === 0) {
|
||||
return {
|
||||
topStudents: [],
|
||||
lowAttendanceStudents: []
|
||||
}
|
||||
}
|
||||
|
||||
// Собираем данные о всех студентах (только для прошедших лекций)
|
||||
pastLessons.forEach(lesson => {
|
||||
lesson.students?.forEach(student => {
|
||||
const studentId = student.sub
|
||||
const current = studentsMap.get(studentId) || {
|
||||
@ -147,9 +159,9 @@ export const CourseCard = ({ course }: { course: Course }) => {
|
||||
})
|
||||
})
|
||||
|
||||
// Для каждого студента установить общее количество лекций
|
||||
// Для каждого студента установить общее количество лекций (только прошедших)
|
||||
studentsMap.forEach(student => {
|
||||
student.total = lessonList.length
|
||||
student.total = pastLessons.length
|
||||
student.percent = (student.attended / student.total) * 100
|
||||
})
|
||||
|
||||
|
@ -11,6 +11,11 @@ import {
|
||||
MenuItem,
|
||||
MenuList,
|
||||
useToast,
|
||||
Flex,
|
||||
Text,
|
||||
useColorMode,
|
||||
Box,
|
||||
Image,
|
||||
} from '@chakra-ui/react'
|
||||
import { EditIcon } from '@chakra-ui/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -32,6 +37,7 @@ type ItemProps = {
|
||||
setlessonToDelete(): void
|
||||
setEditLesson?: () => void
|
||||
students: unknown[]
|
||||
isMobile?: boolean
|
||||
}
|
||||
|
||||
export const Item: React.FC<ItemProps> = ({
|
||||
@ -43,6 +49,7 @@ export const Item: React.FC<ItemProps> = ({
|
||||
setlessonToDelete,
|
||||
setEditLesson,
|
||||
students,
|
||||
isMobile = false,
|
||||
}) => {
|
||||
const [edit, setEdit] = useState(false)
|
||||
const toastRef = useRef(null)
|
||||
@ -50,6 +57,23 @@ export const Item: React.FC<ItemProps> = ({
|
||||
const [updateLesson, updateLessonRqst] = api.useUpdateLessonMutation()
|
||||
const createdLessonRef = useRef(null)
|
||||
const { t } = useTranslation()
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
// QR-код с применением фильтра инверсии для тёмной темы
|
||||
const QRCodeImage = () => (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
filter={colorMode === 'dark' ? 'invert(1)' : 'none'}
|
||||
>
|
||||
<img
|
||||
width={isMobile ? 20 : 24}
|
||||
src={qrCode}
|
||||
alt="QR код"
|
||||
style={{ margin: '0 auto' }}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
||||
const onSubmit = (lessonData) => {
|
||||
toastRef.current = toast({
|
||||
@ -104,6 +128,58 @@ export const Item: React.FC<ItemProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
<Flex justify="space-between" align="center" mb={2}>
|
||||
<Text fontWeight="medium">{name}</Text>
|
||||
<Text fontSize="sm">{dayjs(date).format(groupByDate ? 'HH:mm' : 'HH:mm DD.MM.YY')}</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="space-between" align="center">
|
||||
{isTeacher && (
|
||||
<Link
|
||||
to={`${getNavigationValue('journal.main')}/lesson/${courseId}/${id}`}
|
||||
style={{ display: 'flex' }}
|
||||
>
|
||||
<QRCodeImage />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Flex align="center">
|
||||
<Text fontSize="sm" mr={2}>
|
||||
{t('journal.pl.common.marked')}: {students.length}
|
||||
</Text>
|
||||
|
||||
{isTeacher && !edit && (
|
||||
<Menu>
|
||||
<MenuButton as={Button} size="sm">
|
||||
<EditIcon />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (setEditLesson) {
|
||||
setEditLesson();
|
||||
} else {
|
||||
setEdit(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('journal.pl.edit')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={setlessonToDelete}>{t('journal.pl.delete')}</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
{edit && <Button size="sm" onClick={setlessonToDelete}>{t('journal.pl.save')}</Button>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Стандартное отображение
|
||||
return (
|
||||
<Tr>
|
||||
{isTeacher && (
|
||||
@ -112,7 +188,7 @@ export const Item: React.FC<ItemProps> = ({
|
||||
to={`${getNavigationValue('journal.main')}/lesson/${courseId}/${id}`}
|
||||
style={{ display: 'flex' }}
|
||||
>
|
||||
<img width={24} src={qrCode} style={{ margin: '0 auto' }} />
|
||||
<QRCodeImage />
|
||||
</Link>
|
||||
</Td>
|
||||
)}
|
||||
|
@ -3,6 +3,10 @@ import dayjs from 'dayjs'
|
||||
import {
|
||||
Tr,
|
||||
Td,
|
||||
Box,
|
||||
Flex,
|
||||
Text,
|
||||
useBreakpointValue,
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
import { Lesson } from '../../../__data__/model'
|
||||
@ -25,24 +29,70 @@ export const LessonItems: React.FC<LessonItemProps> = ({
|
||||
courseId,
|
||||
setlessonToDelete,
|
||||
setEditLesson,
|
||||
}) => (
|
||||
<>
|
||||
{date && (
|
||||
<Tr>
|
||||
<Td colSpan={isTeacher ? 5 : 3}>
|
||||
{dayjs(date).format('DD MMMM YYYY')}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{lessons.map((lesson) => (
|
||||
<Item
|
||||
key={lesson.id}
|
||||
{...lesson}
|
||||
setlessonToDelete={() => setlessonToDelete(lesson)}
|
||||
setEditLesson={setEditLesson ? () => setEditLesson(lesson) : undefined}
|
||||
courseId={courseId}
|
||||
isTeacher={isTeacher}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}) => {
|
||||
// Использование useBreakpointValue для определения мобильного отображения
|
||||
const isMobile = useBreakpointValue({ base: true, md: false })
|
||||
|
||||
// Мобильное отображение
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
{date && (
|
||||
<Box
|
||||
p={3}
|
||||
mb={2}
|
||||
bg="gray.100"
|
||||
borderRadius="md"
|
||||
_dark={{ bg: "gray.700" }}
|
||||
>
|
||||
<Text fontWeight="bold">{dayjs(date).format('DD MMMM YYYY')}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{lessons.map((lesson) => (
|
||||
<Box
|
||||
key={lesson.id}
|
||||
p={3}
|
||||
mb={2}
|
||||
borderRadius="md"
|
||||
boxShadow="sm"
|
||||
borderLeft="4px solid"
|
||||
borderLeftColor="cyan.500"
|
||||
>
|
||||
<Item
|
||||
{...lesson}
|
||||
setlessonToDelete={() => setlessonToDelete(lesson)}
|
||||
setEditLesson={setEditLesson ? () => setEditLesson(lesson) : undefined}
|
||||
courseId={courseId}
|
||||
isTeacher={isTeacher}
|
||||
isMobile={true}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Стандартное отображение для планшетов и больших экранов
|
||||
return (
|
||||
<>
|
||||
{date && (
|
||||
<Tr>
|
||||
<Td colSpan={isTeacher ? 5 : 3}>
|
||||
{dayjs(date).format('DD MMMM YYYY')}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{lessons.map((lesson) => (
|
||||
<Item
|
||||
key={lesson.id}
|
||||
{...lesson}
|
||||
setlessonToDelete={() => setlessonToDelete(lesson)}
|
||||
setEditLesson={setEditLesson ? () => setEditLesson(lesson) : undefined}
|
||||
courseId={courseId}
|
||||
isTeacher={isTeacher}
|
||||
isMobile={false}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
useBreakpointValue,
|
||||
} from '@chakra-ui/react'
|
||||
import { AddIcon } from '@chakra-ui/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -257,6 +258,9 @@ const LessonList = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем определение размера экрана
|
||||
const isMobile = useBreakpointValue({ base: true, md: false })
|
||||
|
||||
if (isLoading) {
|
||||
return <XlSpinner />
|
||||
}
|
||||
@ -352,38 +356,54 @@ const LessonList = () => {
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<TableContainer whiteSpace="wrap" pb={13}>
|
||||
<Table variant="striped" colorScheme="cyan">
|
||||
<Thead>
|
||||
<Tr>
|
||||
{isTeacher(user) && (
|
||||
<Th align="center" width={1}>
|
||||
{t('journal.pl.lesson.link')}
|
||||
{isMobile ? (
|
||||
<Box pb={13}>
|
||||
{lessonCalc?.map(({ data: lessons, date }) => (
|
||||
<LessonItems
|
||||
courseId={courseId}
|
||||
date={date}
|
||||
isTeacher={isTeacher(user)}
|
||||
lessons={lessons}
|
||||
setlessonToDelete={setlessonToDelete}
|
||||
setEditLesson={handleEditLesson}
|
||||
key={date}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer whiteSpace="wrap" pb={13}>
|
||||
<Table variant="striped" colorScheme="cyan">
|
||||
<Thead>
|
||||
<Tr>
|
||||
{isTeacher(user) && (
|
||||
<Th align="center" width={1}>
|
||||
{t('journal.pl.lesson.link')}
|
||||
</Th>
|
||||
)}
|
||||
<Th textAlign="center" width={1}>
|
||||
{groupByDate ? t('journal.pl.lesson.time') : t('journal.pl.common.date')}
|
||||
</Th>
|
||||
)}
|
||||
<Th textAlign="center" width={1}>
|
||||
{groupByDate ? t('journal.pl.lesson.time') : t('journal.pl.common.date')}
|
||||
</Th>
|
||||
<Th width="100%">{t('journal.pl.common.name')}</Th>
|
||||
{isTeacher(user) && <Th>{t('journal.pl.lesson.action')}</Th>}
|
||||
<Th isNumeric>{t('journal.pl.common.marked')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{lessonCalc?.map(({ data: lessons, date }) => (
|
||||
<LessonItems
|
||||
courseId={courseId}
|
||||
date={date}
|
||||
isTeacher={isTeacher(user)}
|
||||
lessons={lessons}
|
||||
setlessonToDelete={setlessonToDelete}
|
||||
setEditLesson={handleEditLesson}
|
||||
key={date}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Th width="100%">{t('journal.pl.common.name')}</Th>
|
||||
{isTeacher(user) && <Th>{t('journal.pl.lesson.action')}</Th>}
|
||||
<Th isNumeric>{t('journal.pl.common.marked')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{lessonCalc?.map(({ data: lessons, date }) => (
|
||||
<LessonItems
|
||||
courseId={courseId}
|
||||
date={date}
|
||||
isTeacher={isTeacher(user)}
|
||||
lessons={lessons}
|
||||
setlessonToDelete={setlessonToDelete}
|
||||
setEditLesson={handleEditLesson}
|
||||
key={date}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user