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"
             },