Обновлены компоненты для учета только прошедших лекций в статистике посещаемости. Добавлено мобильное отображение в компонентах LessonItems и Item, улучшена логика фильтрации лекций. Реализовано отображение QR-кода с учетом темы оформления.

This commit is contained in:
Primakov Alexandr Alexandrovich 2025-03-23 17:14:53 +03:00
parent d13bff5331
commit 1b337278fe
5 changed files with 223 additions and 60 deletions

View File

@ -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

View File

@ -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
})

View File

@ -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>
)}

View File

@ -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}
/>
))}
</>
)
}

View File

@ -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>
</>
)