From 8a66b9659900ac9990a0795b87af6ef8b45d3499 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich <primakov.pro@yandex.ru> Date: Wed, 26 Mar 2025 23:41:05 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=B4=D0=BD=D0=B5?= =?UTF-8?q?=D0=B9=20=D0=BD=D0=B5=D0=B4=D0=B5=D0=BB=D0=B8=20=D0=B8=20=D0=BC?= =?UTF-8?q?=D0=B5=D1=81=D1=8F=D1=86=D0=B5=D0=B2,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20=D0=B4=D0=B0?= =?UTF-8?q?=D1=82=D1=8B=20=D0=B8=20=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D1=85=20=D1=83=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2.=20=D0=92=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B5=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20?= =?UTF-8?q?=D1=83=D1=80=D0=BE=D0=BA=D0=BE=D0=B2=20=D1=80=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BA=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B4=D0=B0=D1=80=D1=8C=20=D0=B4=D0=BB=D1=8F=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20=D0=B4=D0=B0=D1=82=D1=8B=20?= =?UTF-8?q?=D1=81=20=D1=83=D1=87=D0=B5=D1=82=D0=BE=D0=BC=20=D1=81=D1=83?= =?UTF-8?q?=D1=89=D0=B5=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D1=85=20?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=86=D0=B8=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.json | 23 +- locales/ru.json | 23 +- .../lesson-list/components/lessons-form.tsx | 266 ++++++++++++++---- src/pages/lesson-list/lesson-list.tsx | 4 + stubs/api/index.js | 2 +- stubs/mocks/courses/by-id/success.json | 4 +- stubs/mocks/courses/by-id/with-exam.json | 4 +- 7 files changed, 271 insertions(+), 55 deletions(-) diff --git a/locales/en.json b/locales/en.json index e086347..4f3afe2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -217,5 +217,26 @@ "journal.pl.days.morning": "Morning", "journal.pl.days.day": "Day", "journal.pl.days.evening": "Evening", - "journal.pl.lesson.form.selectTime": "Select time" + "journal.pl.lesson.form.selectTime": "Select time", + "journal.pl.lesson.existingLessonHint": "There is already a lesson on this day", + "journal.pl.lesson.form.selectDate": "Select date", + "journal.pl.days.shortMonday": "Mo", + "journal.pl.days.shortTuesday": "Tu", + "journal.pl.days.shortWednesday": "We", + "journal.pl.days.shortThursday": "Th", + "journal.pl.days.shortFriday": "Fr", + "journal.pl.days.shortSaturday": "Sa", + "journal.pl.days.shortSunday": "Su", + "journal.pl.months.january": "January", + "journal.pl.months.february": "February", + "journal.pl.months.march": "March", + "journal.pl.months.april": "April", + "journal.pl.months.may": "May", + "journal.pl.months.june": "June", + "journal.pl.months.july": "July", + "journal.pl.months.august": "August", + "journal.pl.months.september": "September", + "journal.pl.months.october": "October", + "journal.pl.months.november": "November", + "journal.pl.months.december": "December" } \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index 0e36fd4..4d751b9 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -214,5 +214,26 @@ "journal.pl.days.morning": "Утро", "journal.pl.days.day": "День", "journal.pl.days.evening": "Вечер", - "journal.pl.lesson.form.selectTime": "Выберите время" + "journal.pl.lesson.form.selectTime": "Выберите время", + "journal.pl.lesson.existingLessonHint": "В этот день уже есть лекция", + "journal.pl.lesson.form.selectDate": "Выберите дату", + "journal.pl.days.shortMonday": "Пн", + "journal.pl.days.shortTuesday": "Вт", + "journal.pl.days.shortWednesday": "Ср", + "journal.pl.days.shortThursday": "Чт", + "journal.pl.days.shortFriday": "Пт", + "journal.pl.days.shortSaturday": "Сб", + "journal.pl.days.shortSunday": "Вс", + "journal.pl.months.january": "Январь", + "journal.pl.months.february": "Февраль", + "journal.pl.months.march": "Март", + "journal.pl.months.april": "Апрель", + "journal.pl.months.may": "Май", + "journal.pl.months.june": "Июнь", + "journal.pl.months.july": "Июль", + "journal.pl.months.august": "Август", + "journal.pl.months.september": "Сентябрь", + "journal.pl.months.october": "Октябрь", + "journal.pl.months.november": "Ноябрь", + "journal.pl.months.december": "Декабрь" } \ No newline at end of file diff --git a/src/pages/lesson-list/components/lessons-form.tsx b/src/pages/lesson-list/components/lessons-form.tsx index 6363a4b..51970cd 100644 --- a/src/pages/lesson-list/components/lessons-form.tsx +++ b/src/pages/lesson-list/components/lessons-form.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { useForm, Controller } from 'react-hook-form' import { Box, @@ -27,9 +27,11 @@ import { useStyleConfig, Select, Wrap, - WrapItem + WrapItem, + IconButton, + Center } from '@chakra-ui/react' -import { AddIcon, CheckIcon, WarningIcon, RepeatIcon } from '@chakra-ui/icons' +import { AddIcon, CheckIcon, WarningIcon, RepeatIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons' import { useTranslation } from 'react-i18next' import { FaRobot } from 'react-icons/fa' import dayjs from 'dayjs' @@ -58,6 +60,7 @@ interface LessonFormProps { onSelectAiSuggestion?: (suggestion: any) => void // Обработчик выбора предложения selectedAiSuggestion?: any // Выбранное предложение onRetryAiGeneration?: () => void // Функция для повторного запуска генерации + existingLessons?: Array<{ date: string; name: string }> // Добавляем новый проп } export const LessonForm = ({ @@ -72,7 +75,8 @@ export const LessonForm = ({ isLoadingAiSuggestions = false, onSelectAiSuggestion = () => {}, selectedAiSuggestion, - onRetryAiGeneration = () => {} + onRetryAiGeneration = () => {}, + existingLessons }: LessonFormProps) => { const { t } = useTranslation() const isAiSuggested = lesson && !lesson._id && !lesson.id @@ -195,6 +199,197 @@ export const LessonForm = ({ return days[date.getDay()]; }; + // Добавляем вспомогательные функции для календаря + const getDaysInMonth = (year: number, month: number) => { + return new Date(year, month + 1, 0).getDate(); + }; + + const getFirstDayOfMonth = (year: number, month: number) => { + return new Date(year, month, 1).getDay(); + }; + + const isWeekend = (dayOfWeek: number) => { + return dayOfWeek === 0 || dayOfWeek === 6; // Воскресенье или суббота + }; + + const isSameDay = (date1: Date, date2: Date) => { + return date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate(); + }; + // Компонент календаря + interface CalendarProps { + selectedDate: Date; + onSelectDate: (date: Date) => void; + existingLessons?: string[]; + } + + const Calendar: React.FC<CalendarProps> = ({ selectedDate, onSelectDate, existingLessons = [] }) => { + const { t } = useTranslation(); + const [viewDate, setViewDate] = useState(new Date()); + + // Используем короткие названия дней недели из локализации + const weekDays = [ + t('journal.pl.days.shortMonday'), + t('journal.pl.days.shortTuesday'), + t('journal.pl.days.shortWednesday'), + t('journal.pl.days.shortThursday'), + t('journal.pl.days.shortFriday'), + t('journal.pl.days.shortSaturday'), + t('journal.pl.days.shortSunday'), + ]; + + // Используем локализованные названия месяцев + const monthNames = [ + t('journal.pl.months.january'), + t('journal.pl.months.february'), + t('journal.pl.months.march'), + t('journal.pl.months.april'), + t('journal.pl.months.may'), + t('journal.pl.months.june'), + t('journal.pl.months.july'), + t('journal.pl.months.august'), + t('journal.pl.months.september'), + t('journal.pl.months.october'), + t('journal.pl.months.november'), + t('journal.pl.months.december'), + ]; + + const daysInMonth = getDaysInMonth(viewDate.getFullYear(), viewDate.getMonth()); + let firstDay = getFirstDayOfMonth(viewDate.getFullYear(), viewDate.getMonth()); + firstDay = firstDay === 0 ? 6 : firstDay - 1; // Корректируем для начала недели с понедельника + + const days = Array.from({ length: 42 }, (_, i) => { + const dayNumber = i - firstDay + 1; + if (dayNumber > 0 && dayNumber <= daysInMonth) { + const date = new Date(viewDate.getFullYear(), viewDate.getMonth(), dayNumber); + return { + date, + dayOfMonth: dayNumber, + isCurrentMonth: true, + isWeekend: isWeekend(date.getDay()), + isToday: isSameDay(date, new Date()), + isSelected: isSameDay(date, selectedDate) + }; + } + return null; + }); + + // Добавим функцию проверки наличия лекции в определенный день + const hasLessonOnDate = (date: Date) => { + return existingLessons.some(lessonDate => + isSameDay(new Date(lessonDate), date) + ); + }; + + return ( + <Box> + <Text fontSize="sm" mb={2}>{t('journal.pl.lesson.form.selectDate')}</Text> + <HStack justify="space-between" mb={2}> + <IconButton + aria-label="Previous month" + icon={<ChevronLeftIcon />} + size="sm" + onClick={() => { + const newDate = new Date(viewDate); + newDate.setMonth(newDate.getMonth() - 1); + setViewDate(newDate); + }} + /> + <HStack> + <Select + size="sm" + value={viewDate.getMonth()} + onChange={(e) => { + const newDate = new Date(viewDate); + newDate.setMonth(parseInt(e.target.value)); + setViewDate(newDate); + }} + > + {monthNames.map((month, i) => ( + <option key={i} value={i}>{month}</option> + ))} + </Select> + <Select + size="sm" + value={viewDate.getFullYear()} + onChange={(e) => { + const newDate = new Date(viewDate); + newDate.setFullYear(parseInt(e.target.value)); + setViewDate(newDate); + }} + > + {Array.from({ length: 5 }, (_, i) => { + const year = new Date().getFullYear() + i; + return <option key={year} value={year}>{year}</option>; + })} + </Select> + </HStack> + <IconButton + aria-label="Next month" + icon={<ChevronRightIcon />} + size="sm" + onClick={() => { + const newDate = new Date(viewDate); + newDate.setMonth(newDate.getMonth() + 1); + setViewDate(newDate); + }} + /> + </HStack> + + <SimpleGrid columns={7} spacing={1}> + {weekDays.map(day => ( + <Center key={day} py={1}> + <Text fontSize="xs" color="gray.500"> + {day} + </Text> + </Center> + ))} + {days.map((day, i) => { + const hasLesson = day?.isCurrentMonth && hasLessonOnDate(day.date); + + return ( + <Button + key={i} + size="sm" + variant={day?.isSelected ? "solid" : "ghost"} + colorScheme={day?.isSelected ? "blue" : day?.isWeekend ? "red" : "gray"} + opacity={day?.isCurrentMonth ? 1 : 0} + onClick={() => day?.date && onSelectDate(day.date)} + h="32px" + disabled={!day?.isCurrentMonth} + position="relative" + _after={hasLesson ? { + content: '""', + position: "absolute", + bottom: "2px", + left: "50%", + transform: "translateX(-50%)", + width: "4px", + height: "4px", + borderRadius: "full", + bg: day?.isSelected ? "white" : "blue.500", + _dark: { + bg: day?.isSelected ? "white" : "blue.300" + } + } : undefined} + title={hasLesson ? t('journal.pl.lesson.existingLessonHint') : undefined} + > + <Text + fontSize="xs" + fontWeight={day?.isToday ? "bold" : "normal"} + textDecoration={day?.isToday ? "underline" : "none"} + > + {day?.dayOfMonth} + </Text> + </Button> + ); + })} + </SimpleGrid> + </Box> + ); + }; + return ( <Card align="left" bg={isAiSuggested ? aiHighlightColor : undefined}> <CardHeader display="flex"> @@ -225,57 +420,33 @@ export const LessonForm = ({ name="date" rules={{ required: t('journal.pl.common.required') }} render={({ field }) => { - // Разделяем текущее значение на дату и время const [currentDate = '', currentTime = '00:00:00'] = field.value.split('T'); - // Получаем часы и минуты без секунд для сравнения const currentTimeShort = currentTime.split(':').slice(0, 2).join(':'); + const selectedDate = new Date(currentDate); + + // Получаем существующие лекции из пропсов компонента + const existingLessons2 = existingLessons?.map(lesson => lesson.date) || []; return ( <FormControl> <FormLabel>{t('journal.pl.lesson.form.date')}</FormLabel> - <VStack align="stretch" spacing={4}> - <HStack spacing={2}> - {[0, 1, 2].map(daysToAdd => { - const date = new Date(); - date.setDate(date.getDate() + daysToAdd); - const formattedDate = dateToCalendarFormat(date.toISOString()).split('T')[0]; - const dayOfWeek = getDayOfWeek(date); - - return ( - <Button - key={daysToAdd} - size="sm" - variant={currentDate === formattedDate ? "solid" : "outline"} - colorScheme="blue" - onClick={() => { - // Сохраняем текущее время при смене даты - field.onChange(`${formattedDate}T${currentTime}:00`); - }} - > - {daysToAdd === 0 ? t('journal.pl.today') : - daysToAdd === 1 ? t('journal.pl.tomorrow') : - t('journal.pl.dayAfterTomorrow')} - <Text as="span" fontSize="xs" ml={1} color="gray.500"> - ({dayOfWeek}) - </Text> - </Button> - ); - })} - </HStack> - - <Input - value={currentDate} - onChange={(e) => { - // При ручном изменении даты сохраняем текущее время - field.onChange(`${e.target.value}T${currentTime}:00`); - }} - type="date" - size="sm" - /> + <SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}> + {/* Календарь */} + <Box> + <Calendar + selectedDate={selectedDate} + existingLessons={existingLessons2} + onSelectDate={(date) => { + const formattedDate = dateToCalendarFormat(date.toISOString()).split('T')[0]; + field.onChange(`${formattedDate}T${currentTimeShort}:00`); + }} + /> + </Box> + {/* Временные слоты */} <Box> <Text fontSize="sm" mb={2}>{t('journal.pl.lesson.form.selectTime')}:</Text> - <SimpleGrid columns={{ base: 1, md: 3 }} spacing={4}> + <SimpleGrid columns={1} spacing={4}> {Object.entries(timeGroups).map(([groupName, slots]) => ( <Box key={groupName}> <Text fontSize="xs" color="gray.500" mb={1}> @@ -284,7 +455,6 @@ export const LessonForm = ({ <Wrap spacing={1}> {slots.map(slot => { const isSelected = currentTimeShort === slot; - return ( <WrapItem key={slot}> <Button @@ -307,7 +477,7 @@ export const LessonForm = ({ ))} </SimpleGrid> </Box> - </VStack> + </SimpleGrid> </FormControl> ); }} diff --git a/src/pages/lesson-list/lesson-list.tsx b/src/pages/lesson-list/lesson-list.tsx index b8675df..2c929bd 100644 --- a/src/pages/lesson-list/lesson-list.tsx +++ b/src/pages/lesson-list/lesson-list.tsx @@ -359,6 +359,10 @@ const LessonList = () => { onSelectAiSuggestion={handleSelectAiSuggestion} selectedAiSuggestion={suggestedLessonToCreate} onRetryAiGeneration={handleRetryAiGeneration} + existingLessons={data?.body?.map(lesson => ({ + date: lesson.date, + name: lesson.name + }))} /> ) : ( <Button diff --git a/stubs/api/index.js b/stubs/api/index.js index b6b8966..3a7c81a 100644 --- a/stubs/api/index.js +++ b/stubs/api/index.js @@ -35,7 +35,7 @@ function readAndModifyJson(filePath) { jsonContent.body.forEach((lesson) => { // Случайная дата в пределах последних 3 месяцев const randomDate = new Date(); - randomDate.setMonth(randomDate.getMonth() - Math.random() * 3); + randomDate.setDate(randomDate.getDate() - Math.random() * 30); lesson.date = randomDate.toISOString(); lesson.created = new Date(randomDate.getTime() - 86400000).toISOString(); // Создан за день до даты }); diff --git a/stubs/mocks/courses/by-id/success.json b/stubs/mocks/courses/by-id/success.json index c166b2f..2fa82aa 100644 --- a/stubs/mocks/courses/by-id/success.json +++ b/stubs/mocks/courses/by-id/success.json @@ -590,8 +590,8 @@ "sub": "developer", "email": "email@email.ml" }, - "startDt": "2024-08-25T17:40:17.814Z", - "created": "2024-08-25T17:40:17.814Z", + "startDt": "2024-08-25T17:30:00.000Z", + "created": "2024-08-25T17:40:17.000Z", "examWithJury2": { "_id": "66cf3d3f4637d420d6271451", "name": "Хакатон", diff --git a/stubs/mocks/courses/by-id/with-exam.json b/stubs/mocks/courses/by-id/with-exam.json index 9c3e1b6..e828814 100644 --- a/stubs/mocks/courses/by-id/with-exam.json +++ b/stubs/mocks/courses/by-id/with-exam.json @@ -19,7 +19,7 @@ "email": "primakovpro@gmail.com" } ], - "date": "2024-04-16T13:38:00.000Z", + "date": "2024-04-16T13:30:00.000Z", "created": "2024-04-16T13:38:23.381Z", "id": "661e7f4f69f40b0ebebcd5e4" }, @@ -37,7 +37,7 @@ "email": "primakovpro@gmail.com" } ], - "date": "2024-08-04T07:00:00.000Z", + "date": "2024-08-04T08:00:00.000Z", "created": "2024-08-04T06:23:28.491Z", "id": "66af1e60a0eef5a89f99aa94" },