feat: добавлена полная поддержка редактирования повторяющихся событий
- Реализована возможность конвертации повторяющегося события в обычное:
* При снятии галочки 'Повторяющееся событие' 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 операций для повторяющихся событий
This commit is contained in:
parent
fda5452a52
commit
3635eb7759
@ -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 (
|
||||
<button
|
||||
key={day.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = editingEvent.byweekday || [];
|
||||
const current = normalizeByweekday(editingEvent.byweekday);
|
||||
const updated = isSelected
|
||||
? current.filter((d: number) => d !== day.value)
|
||||
: [...current, day.value].sort();
|
||||
|
||||
@ -30,12 +30,12 @@ export function ViewDayEventsModal({
|
||||
const eventStart = new Date(event.start);
|
||||
const eventEnd = new Date(event.end);
|
||||
const selectedDay = new Date(date);
|
||||
|
||||
|
||||
// Сбрасываем время для корректного сравнения дат
|
||||
eventStart.setHours(0, 0, 0, 0);
|
||||
eventEnd.setHours(0, 0, 0, 0);
|
||||
selectedDay.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
// Проверяем, попадает ли selectedDay в диапазон [eventStart, eventEnd]
|
||||
return selectedDay >= eventStart && selectedDay <= eventEnd;
|
||||
});
|
||||
@ -100,7 +100,7 @@ export function ViewDayEventsModal({
|
||||
</h4>
|
||||
{event.rule && (
|
||||
<span className="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded flex-shrink-0">
|
||||
🔁
|
||||
⟲
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -37,6 +37,17 @@ const WEEKDAY_LABELS: Record<number, string> = {
|
||||
6: "ВС",
|
||||
};
|
||||
|
||||
// Нормализация 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 [];
|
||||
}
|
||||
|
||||
export function ViewEventDetailsModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
@ -97,7 +108,7 @@ export function ViewEventDetailsModal({
|
||||
</h3>
|
||||
{event.rule && (
|
||||
<span className="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded flex-shrink-0">
|
||||
🔁
|
||||
⟲
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -146,7 +157,7 @@ export function ViewEventDetailsModal({
|
||||
{event.rule_data.params?.byweekday && (
|
||||
<div>
|
||||
Дни недели: <span className="font-medium">
|
||||
{event.rule_data.params.byweekday.map((day: number) => WEEKDAY_LABELS[day]).join(", ")}
|
||||
{normalizeByweekday(event.rule_data.params.byweekday).map((day: number) => WEEKDAY_LABELS[day]).join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@ -161,9 +172,7 @@ export function ViewEventDetailsModal({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-blue-600 mt-2">
|
||||
💡 Редактирование параметров повторения пока не поддерживается
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -530,6 +530,24 @@ class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
async updateRule(ruleId: number, data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
frequency: string;
|
||||
params?: any;
|
||||
}): Promise<any> {
|
||||
// Сериализуем params в JSON-строку для django-scheduler
|
||||
const payload = {
|
||||
...data,
|
||||
params: data.params ? JSON.stringify(data.params) : '{}',
|
||||
};
|
||||
|
||||
return this.request(`/api/v1/schedule/rules/${ruleId}/`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
async getRules(): Promise<any> {
|
||||
return this.request('/api/v1/schedule/rules/');
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user