From 3635eb7759dd073c3c262b115b1084e50333da33 Mon Sep 17 00:00:00 2001 From: igor_nadein Date: Wed, 25 Feb 2026 03:02:59 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B0=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE=D1=80=D1=8F?= =?UTF-8?q?=D1=8E=D1=89=D0=B8=D1=85=D1=81=D1=8F=20=D1=81=D0=BE=D0=B1=D1=8B?= =?UTF-8?q?=D1=82=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Реализована возможность конвертации повторяющегося события в обычное: * При снятии галочки 'Повторяющееся событие' rule_id сбрасывается в null * Событие становится одиночным, все occurrence исчезают - Добавлен метод updateRule() в apiClient: * Позволяет обновлять существующие правила повторения * Изменения применяются ко всем occurrence события * Использует PUT запрос к /api/v1/schedule/rules/{id}/ - Исправлено использование repeatMode: * Заменен устаревший useCount на repeatMode ('until' | 'count' | 'forever') * Корректная обработка end_recurring_period только для режима 'until' - Добавлена универсальная функция normalizeByweekday(): * Решена проблема с разными форматами byweekday с backend: - Массив: [0, 4] ✓ - Строка: '0,4' ✓ - Число: 4 ✓ * Функция используется во всех компонентах: - EventModal: авто-выбор дней, отображение кнопок, сохранение - ViewEventDetailsModal: отображение дней недели * Исправлены ошибки '.map is not a function' и '.includes is not a function' - Улучшена логика handleSave() в EventModal: * Проверка необходимости сброса rule при снятии галочки * Разделение логики создания и обновления правил * Нормализация byweekday перед отправкой на backend Closes: полная поддержка CRUD операций для повторяющихся событий --- frontend/src/components/EventModal.tsx | 63 +++++++++++++------ .../src/components/ViewDayEventsModal.tsx | 6 +- .../src/components/ViewEventDetailsModal.tsx | 19 ++++-- frontend/src/lib/api.ts | 18 ++++++ 4 files changed, 80 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/EventModal.tsx b/frontend/src/components/EventModal.tsx index 49294ab5..37c171aa 100644 --- a/frontend/src/components/EventModal.tsx +++ b/frontend/src/components/EventModal.tsx @@ -4,6 +4,17 @@ import { useState, useEffect, useRef } from "react"; import { X, Trash2 } from "lucide-react"; import { apiClient } from "@/lib/api"; +// Нормализация byweekday в массив чисел +function normalizeByweekday(byweekday: any): number[] { + if (!byweekday) return []; + if (Array.isArray(byweekday)) return byweekday; + if (typeof byweekday === 'string') { + return byweekday.split(',').map(d => parseInt(d.trim(), 10)).filter(n => !isNaN(n)); + } + if (typeof byweekday === 'number') return [byweekday]; + return []; +} + interface EventModalProps { isOpen: boolean; onClose: () => void; @@ -56,7 +67,8 @@ export function EventModal({ const dayIndex = startDay === 0 ? 6 : startDay - 1; // Если дни недели еще не выбраны - добавляем день начала - if (!editingEvent.byweekday || editingEvent.byweekday.length === 0) { + const normalizedByweekday = normalizeByweekday(editingEvent.byweekday); + if (normalizedByweekday.length === 0) { setEditingEvent((prev: any) => ({ ...prev, byweekday: [dayIndex] @@ -123,7 +135,8 @@ export function EventModal({ // Вычисляет дату первого вхождения еженедельного события const getFirstOccurrenceDate = (): string | null => { - if (editingEvent?.frequency !== 'WEEKLY' || !editingEvent?.start || !editingEvent?.byweekday || editingEvent.byweekday.length === 0) { + const normalizedByweekday = normalizeByweekday(editingEvent?.byweekday); + if (editingEvent?.frequency !== 'WEEKLY' || !editingEvent?.start || normalizedByweekday.length === 0) { return null; } @@ -132,12 +145,12 @@ export function EventModal({ const startDayIndex = startDay === 0 ? 6 : startDay - 1; // Если день начала выбран - первое вхождение это сам день начала - if (editingEvent.byweekday.includes(startDayIndex)) { + if (normalizedByweekday.includes(startDayIndex)) { return null; // Не показываем подсказку } // Ищем ближайший выбранный день недели - const sortedDays = [...editingEvent.byweekday].sort((a, b) => a - b); + const sortedDays = [...normalizedByweekday].sort((a, b) => a - b); let daysToAdd = 0; // Ищем день после startDayIndex @@ -192,7 +205,7 @@ export function EventModal({ isRecurring: true, frequency: rule.frequency, interval: rule.params?.interval || 1, - byweekday: rule.params?.byweekday || [], + byweekday: normalizeByweekday(rule.params?.byweekday), count: rule.params?.count || undefined, repeatMode, }; @@ -265,29 +278,43 @@ export function EventModal({ setSaving(true); let ruleId = editingEvent.rule || null; - // Если это повторяющееся событие и у него нет Rule - создаем правило - if (editingEvent.isRecurring && !ruleId) { + // Если убрали галочку "Повторяющееся событие" - сбрасываем rule + if (!editingEvent.isRecurring && editingEvent.id && editingEvent.rule) { + ruleId = null; + } + + // Если это повторяющееся событие - создаем или обновляем правило + if (editingEvent.isRecurring) { const params: any = { interval: editingEvent.interval || 1, }; // Добавляем byweekday для еженедельных событий - if (editingEvent.frequency === 'WEEKLY' && editingEvent.byweekday && editingEvent.byweekday.length > 0) { - params.byweekday = editingEvent.byweekday; + const normalizedByweekday = normalizeByweekday(editingEvent.byweekday); + if (editingEvent.frequency === 'WEEKLY' && normalizedByweekday.length > 0) { + params.byweekday = normalizedByweekday; } - // Добавляем count если выбран этот вариант - if (editingEvent.useCount && editingEvent.count) { + // Добавляем count если выбран режим "Количество раз" + if (editingEvent.repeatMode === 'count' && editingEvent.count) { params.count = editingEvent.count; } - const rule = await apiClient.createRule({ + const ruleData = { name: `Rule for ${editingEvent.title}`, description: `Recurring rule for ${editingEvent.title}`, frequency: editingEvent.frequency || 'WEEKLY', params, - }); - ruleId = rule.id; + }; + + if (ruleId) { + // Обновляем существующее правило + await apiClient.updateRule(ruleId, ruleData); + } else { + // Создаем новое правило + const rule = await apiClient.createRule(ruleData); + ruleId = rule.id; + } } const eventData: any = { @@ -304,8 +331,8 @@ export function EventModal({ rule: ruleId, }; - // Добавляем end_recurring_period только для повторяющихся событий (если не используется count) - if (editingEvent.isRecurring && !editingEvent.useCount && editingEvent.end_recurring_period) { + // Добавляем end_recurring_period только для повторяющихся событий с режимом 'until' + if (editingEvent.isRecurring && editingEvent.repeatMode === 'until' && editingEvent.end_recurring_period) { const endDate = new Date(editingEvent.end_recurring_period); endDate.setHours(23, 59, 59, 999); eventData.end_recurring_period = endDate.toISOString(); @@ -532,14 +559,14 @@ export function EventModal({ { value: 5, label: 'СБ' }, { value: 6, label: 'ВС' }, ].map((day) => { - const byweekday = editingEvent.byweekday || []; + const byweekday = normalizeByweekday(editingEvent.byweekday); const isSelected = byweekday.includes(day.value); return (