feat(calendar): улучшенный UX - модалы просмотра перед действиями
Новые компоненты: - ViewDayEventsModal: показывает все события в выбранный день - Список событий с временем и описанием - Кнопка "Создать событие на этот день" - Клик на событие → ViewEventDetailsModal - ViewEventDetailsModal: детали события (read-only) - Все поля события (время, описание, участники) - Индикаторы повторяющихся событий � - Кнопки "Редактировать" и "Удалить" Изменения в BigCalendar: - handleSelectSlot: клик на дату → открывает ViewDayEventsModal вместо прямого создания - handleSelectEvent: клик на событие → открывает ViewEventDetailsModal вместо прямого редактирования - Добавлены обработчики переходов между модалами Изменения в AppShell: - handleOpenEventModal: логика маршрутизации модалов (дата/событие/создание) - Добавлены обработчики для новых модалов - sidebarEvents: состояние для передачи событий в ViewDayEventsModal - Интеграция в CalendarCard для обоих календарей (desktop + mobile) Преимущества UX: - Пользователь видит контекст перед действием - Предотвращение случайных дубликатов событий - Логичный flow: Просмотр → Действие (create/edit/delete) - Особенно полезно для повторяющихся событий
This commit is contained in:
parent
b490b4e82a
commit
be70822945
@ -10,6 +10,8 @@ import { CalendarProvider, useCalendar } from "@/contexts/CalendarContext";
|
||||
import { CalendarModal } from "@/components/CalendarModal";
|
||||
import CalendarParticipantsModal from "@/components/CalendarParticipantsModal";
|
||||
import { EventModal } from "@/components/EventModal";
|
||||
import { ViewDayEventsModal } from "@/components/ViewDayEventsModal";
|
||||
import { ViewEventDetailsModal } from "@/components/ViewEventDetailsModal";
|
||||
|
||||
type AppShellProps = {
|
||||
children: ReactNode;
|
||||
@ -299,13 +301,15 @@ function CalendarCard({
|
||||
onOpenEventModal,
|
||||
onOpenParticipantsModal,
|
||||
eventsRefreshTrigger,
|
||||
setEventsRefreshTrigger
|
||||
setEventsRefreshTrigger,
|
||||
setSidebarEvents
|
||||
}: {
|
||||
onOpenCalendarModal: (calendar?: { id?: number; name: string }) => void;
|
||||
onOpenEventModal: (event: any, date?: Date) => void;
|
||||
onOpenParticipantsModal: (calendar: { id: number; name: string; user_role?: string }) => void;
|
||||
eventsRefreshTrigger: number;
|
||||
setEventsRefreshTrigger: (value: number | ((prev: number) => number)) => void;
|
||||
setSidebarEvents: (events: any[]) => void;
|
||||
}) {
|
||||
const { calendars, selectedCalendarId, setSelectedCalendarId, loading: calendarsLoading, reloadCalendars } = useCalendar();
|
||||
|
||||
@ -353,6 +357,7 @@ function CalendarCard({
|
||||
if (!cancelled) {
|
||||
const eventsList = Array.isArray(myEventsResult) ? myEventsResult : (myEventsResult?.results || []);
|
||||
setEvents(eventsList);
|
||||
setSidebarEvents(eventsList); // Передаем события наружу
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -382,7 +387,9 @@ function CalendarCard({
|
||||
const recurringOccurrences = occurrencesList.filter((occ: any) => occ.is_recurring);
|
||||
|
||||
// Объединяем оба типа событий
|
||||
setEvents([...regularEvents, ...recurringOccurrences]);
|
||||
const allEvents = [...regularEvents, ...recurringOccurrences];
|
||||
setEvents(allEvents);
|
||||
setSidebarEvents(allEvents); // Передаем события наружу для ViewDayEventsModal
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
@ -803,13 +810,15 @@ function Calendar({
|
||||
onOpenEventModal,
|
||||
onOpenParticipantsModal,
|
||||
eventsRefreshTrigger,
|
||||
setEventsRefreshTrigger
|
||||
setEventsRefreshTrigger,
|
||||
setSidebarEvents
|
||||
}: {
|
||||
onOpenCalendarModal: (calendar?: { id?: number; name: string }) => void;
|
||||
onOpenEventModal: (event: any, date?: Date) => void;
|
||||
onOpenParticipantsModal: (calendar: { id: number; name: string; user_role?: string }) => void;
|
||||
eventsRefreshTrigger: number;
|
||||
setEventsRefreshTrigger: (value: number | ((prev: number) => number)) => void;
|
||||
setSidebarEvents: (events: any[]) => void;
|
||||
}) {
|
||||
return (
|
||||
<aside className="hidden w-72 flex-shrink-0 space-y-4 lg:block">
|
||||
@ -820,6 +829,7 @@ function Calendar({
|
||||
onOpenParticipantsModal={onOpenParticipantsModal}
|
||||
eventsRefreshTrigger={eventsRefreshTrigger}
|
||||
setEventsRefreshTrigger={setEventsRefreshTrigger}
|
||||
setSidebarEvents={setSidebarEvents}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
@ -860,6 +870,13 @@ export function AppShell({ children }: AppShellProps) {
|
||||
const [editingEvent, setEditingEvent] = useState<any>(null);
|
||||
const [participantsCalendar, setParticipantsCalendar] = useState<{ id: number; name: string; user_role?: string } | null>(null);
|
||||
const [eventsRefreshTrigger, setEventsRefreshTrigger] = useState(0);
|
||||
|
||||
// Новые модалы для просмотра
|
||||
const [showDayEventsModal, setShowDayEventsModal] = useState(false);
|
||||
const [selectedDateForModal, setSelectedDateForModal] = useState<Date | null>(null);
|
||||
const [showEventDetailsModal, setShowEventDetailsModal] = useState(false);
|
||||
const [viewingEvent, setViewingEvent] = useState<any>(null);
|
||||
const [sidebarEvents, setSidebarEvents] = useState<any[]>([]); // Для ViewDayEventsModal
|
||||
|
||||
// Обработчики модалов
|
||||
const handleOpenCalendarModal = (calendar?: { id?: number; name: string }) => {
|
||||
@ -868,10 +885,75 @@ export function AppShell({ children }: AppShellProps) {
|
||||
};
|
||||
|
||||
const handleOpenEventModal = (event: any, date?: Date) => {
|
||||
setEditingEvent(event);
|
||||
setShowEventModal(true);
|
||||
// Если передана дата (клик на дату), открываем модал просмотра дня
|
||||
if (date && !event.id) {
|
||||
setSelectedDateForModal(date);
|
||||
setShowDayEventsModal(true);
|
||||
}
|
||||
// Если передано событие с id (клик на событие), открываем детали
|
||||
else if (event.id) {
|
||||
setViewingEvent(event);
|
||||
setShowEventDetailsModal(true);
|
||||
}
|
||||
// Fallback - создание нового события
|
||||
else {
|
||||
setEditingEvent(event);
|
||||
setShowEventModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Создание события из модала просмотра дня
|
||||
const handleCreateEventFromDay = useCallback(() => {
|
||||
if (!selectedDateForModal) return;
|
||||
|
||||
const startDate = new Date(selectedDateForModal);
|
||||
startDate.setHours(10, 0, 0, 0);
|
||||
const endDate = new Date(selectedDateForModal);
|
||||
endDate.setHours(11, 0, 0, 0);
|
||||
|
||||
setEditingEvent({
|
||||
title: "",
|
||||
description: "",
|
||||
start: startDate.toISOString(),
|
||||
end: endDate.toISOString(),
|
||||
calendar: null,
|
||||
color_event: "#3498db",
|
||||
});
|
||||
|
||||
setShowDayEventsModal(false);
|
||||
setShowEventModal(true);
|
||||
}, [selectedDateForModal]);
|
||||
|
||||
// Переход к редактированию из модала просмотра
|
||||
const handleEditFromDetails = useCallback(() => {
|
||||
setEditingEvent(viewingEvent);
|
||||
setShowEventDetailsModal(false);
|
||||
setShowEventModal(true);
|
||||
}, [viewingEvent]);
|
||||
|
||||
// Клик на событие из модала просмотра дня
|
||||
const handleEventClickFromDay = useCallback(async (event: any) => {
|
||||
setShowDayEventsModal(false);
|
||||
|
||||
// Если это occurrence, загружаем базовое событие
|
||||
if (event.is_recurring && event.event_id) {
|
||||
try {
|
||||
const fullEvent = await apiClient.getEvent(event.event_id);
|
||||
setViewingEvent({
|
||||
...fullEvent,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
});
|
||||
setShowEventDetailsModal(true);
|
||||
} catch (err) {
|
||||
console.error("Ошибка загрузки события:", err);
|
||||
}
|
||||
} else {
|
||||
setViewingEvent(event);
|
||||
setShowEventDetailsModal(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEventSaved = useCallback(() => {
|
||||
// Обновляем список событий в CalendarCard
|
||||
setEventsRefreshTrigger(prev => prev + 1);
|
||||
@ -953,6 +1035,7 @@ export function AppShell({ children }: AppShellProps) {
|
||||
onOpenParticipantsModal={handleOpenParticipantsModal}
|
||||
eventsRefreshTrigger={eventsRefreshTrigger}
|
||||
setEventsRefreshTrigger={setEventsRefreshTrigger}
|
||||
setSidebarEvents={setSidebarEvents}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1010,6 +1093,7 @@ export function AppShell({ children }: AppShellProps) {
|
||||
onOpenParticipantsModal={handleOpenParticipantsModal}
|
||||
eventsRefreshTrigger={eventsRefreshTrigger}
|
||||
setEventsRefreshTrigger={setEventsRefreshTrigger}
|
||||
setSidebarEvents={setSidebarEvents}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -1049,6 +1133,45 @@ export function AppShell({ children }: AppShellProps) {
|
||||
onSave={handleEventSaved}
|
||||
showParticipants={true}
|
||||
/>
|
||||
|
||||
{/* Модальное окно просмотра событий дня */}
|
||||
<ViewDayEventsModal
|
||||
isOpen={showDayEventsModal}
|
||||
onClose={() => {
|
||||
setShowDayEventsModal(false);
|
||||
setSelectedDateForModal(null);
|
||||
}}
|
||||
date={selectedDateForModal}
|
||||
events={sidebarEvents}
|
||||
onEventClick={handleEventClickFromDay}
|
||||
onCreateEvent={handleCreateEventFromDay}
|
||||
/>
|
||||
|
||||
{/* Модальное окно просмотра деталей события */}
|
||||
<ViewEventDetailsModal
|
||||
isOpen={showEventDetailsModal}
|
||||
onClose={() => {
|
||||
setShowEventDetailsModal(false);
|
||||
setViewingEvent(null);
|
||||
}}
|
||||
event={viewingEvent}
|
||||
onEdit={handleEditFromDetails}
|
||||
onDelete={async () => {
|
||||
if (!viewingEvent?.id) return;
|
||||
if (!confirm("Удалить это событие?")) return;
|
||||
|
||||
try {
|
||||
await apiClient.deleteEvent(viewingEvent.id);
|
||||
setShowEventDetailsModal(false);
|
||||
setViewingEvent(null);
|
||||
handleEventSaved(); // Обновляем список событий
|
||||
} catch (err) {
|
||||
console.error("Ошибка удаления события:", err);
|
||||
alert("Не удалось удалить событие");
|
||||
}
|
||||
}}
|
||||
showParticipants={true}
|
||||
/>
|
||||
</div>
|
||||
</CalendarProvider>
|
||||
);
|
||||
|
||||
@ -9,6 +9,8 @@ import { useCalendar } from "@/contexts/CalendarContext";
|
||||
import { X, Plus, Trash2, Settings, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { CalendarModal } from "@/components/CalendarModal";
|
||||
import { EventModal } from "@/components/EventModal";
|
||||
import { ViewDayEventsModal } from "@/components/ViewDayEventsModal";
|
||||
import { ViewEventDetailsModal } from "@/components/ViewEventDetailsModal";
|
||||
import "react-big-calendar/lib/css/react-big-calendar.css";
|
||||
import "../app/calendar/calendar.css";
|
||||
|
||||
@ -132,6 +134,12 @@ export function BigCalendar() {
|
||||
const [showCalendarModal, setShowCalendarModal] = useState(false);
|
||||
const [editingCalendar, setEditingCalendar] = useState<{ id?: number; name: string } | null>(null);
|
||||
|
||||
// Новые модалы для просмотра
|
||||
const [showDayEventsModal, setShowDayEventsModal] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
const [showEventDetailsModal, setShowEventDetailsModal] = useState(false);
|
||||
const [viewingEvent, setViewingEvent] = useState<CalendarEvent | null>(null);
|
||||
|
||||
// Загрузка событий
|
||||
const loadEvents = useCallback(async () => {
|
||||
try {
|
||||
@ -232,57 +240,97 @@ export function BigCalendar() {
|
||||
|
||||
// Обработчики календаря
|
||||
const handleSelectSlot = useCallback(
|
||||
({ start, end }: { start: Date; end: Date }) => {
|
||||
if (!selectedCalendarId) {
|
||||
alert("Сначала выберите календарь");
|
||||
return;
|
||||
}
|
||||
|
||||
// Если клик на весь день (00:00 - 00:00), устанавливаем разумное время
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
|
||||
// Проверяем, если время 00:00 (клик на день в режиме месяца)
|
||||
if (startDate.getHours() === 0 && startDate.getMinutes() === 0) {
|
||||
startDate.setHours(10, 0, 0, 0); // 10:00
|
||||
endDate.setHours(11, 0, 0, 0); // 11:00
|
||||
}
|
||||
|
||||
setEditingEvent({
|
||||
title: "",
|
||||
description: "",
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
calendar: selectedCalendarId,
|
||||
color_event: "#3498db",
|
||||
});
|
||||
setShowEventModal(true);
|
||||
({ start }: { start: Date; end: Date }) => {
|
||||
// Открываем модальное окно просмотра событий в этот день
|
||||
setSelectedDate(start);
|
||||
setShowDayEventsModal(true);
|
||||
},
|
||||
[selectedCalendarId]
|
||||
[]
|
||||
);
|
||||
|
||||
// Создание события из модала просмотра дня
|
||||
const handleCreateEventFromDay = useCallback(() => {
|
||||
if (!selectedCalendarId) {
|
||||
alert("Сначала выберите календарь");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedDate) return;
|
||||
|
||||
// Устанавливаем время по умолчанию 10:00 - 11:00
|
||||
const startDate = new Date(selectedDate);
|
||||
startDate.setHours(10, 0, 0, 0);
|
||||
|
||||
const endDate = new Date(selectedDate);
|
||||
endDate.setHours(11, 0, 0, 0);
|
||||
|
||||
setEditingEvent({
|
||||
title: "",
|
||||
description: "",
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
calendar: selectedCalendarId,
|
||||
color_event: "#3498db",
|
||||
});
|
||||
|
||||
// Закрываем модал просмотра дня и открываем модал создания
|
||||
setShowDayEventsModal(false);
|
||||
setShowEventModal(true);
|
||||
}, [selectedCalendarId, selectedDate]);
|
||||
|
||||
const handleSelectEvent = useCallback(async (calEvent: CalendarEvent) => {
|
||||
// Если это occurrence (повторяющееся событие), загружаем базовое событие
|
||||
if (calEvent.isOccurrence && calEvent.event_id) {
|
||||
try {
|
||||
const fullEvent = await apiClient.getEvent(calEvent.event_id);
|
||||
// Устанавливаем время из occurrence, но остальные данные из базового события
|
||||
setEditingEvent({
|
||||
setViewingEvent({
|
||||
...fullEvent,
|
||||
start: calEvent.start,
|
||||
end: calEvent.end,
|
||||
});
|
||||
setShowEventModal(true);
|
||||
setShowEventDetailsModal(true);
|
||||
} catch (err) {
|
||||
console.error("Ошибка загрузки базового события:", err);
|
||||
}
|
||||
} else {
|
||||
// Обычное событие - данные уже полные
|
||||
setEditingEvent(calEvent);
|
||||
setShowEventModal(true);
|
||||
// Обычное событие - открываем детали
|
||||
setViewingEvent(calEvent);
|
||||
setShowEventDetailsModal(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Переход к редактированию из модала просмотра
|
||||
const handleEditFromDetails = useCallback(() => {
|
||||
setEditingEvent(viewingEvent);
|
||||
setShowEventDetailsModal(false);
|
||||
setShowEventModal(true);
|
||||
}, [viewingEvent]);
|
||||
|
||||
// Клик на событие из модала просмотра дня
|
||||
const handleEventClickFromDay = useCallback(async (event: any) => {
|
||||
setShowDayEventsModal(false);
|
||||
|
||||
// Если это occurrence, загружаем базовое событие
|
||||
if (event.isOccurrence && event.event_id) {
|
||||
try {
|
||||
const fullEvent = await apiClient.getEvent(event.event_id);
|
||||
setViewingEvent({
|
||||
...fullEvent,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
});
|
||||
setShowEventDetailsModal(true);
|
||||
} catch (err) {
|
||||
console.error("Ошибка загрузки события:", err);
|
||||
}
|
||||
} else {
|
||||
setViewingEvent(event);
|
||||
setShowEventDetailsModal(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
// Стилизация событий по цвету
|
||||
const eventStyleGetter = (event: CalendarEvent) => {
|
||||
const backgroundColor = event.color_event || "#3498db";
|
||||
@ -414,6 +462,46 @@ export function BigCalendar() {
|
||||
}}
|
||||
showParticipants={true}
|
||||
/>
|
||||
|
||||
{/* Модальное окно просмотра событий дня */}
|
||||
<ViewDayEventsModal
|
||||
isOpen={showDayEventsModal}
|
||||
onClose={() => {
|
||||
setShowDayEventsModal(false);
|
||||
setSelectedDate(null);
|
||||
}}
|
||||
date={selectedDate}
|
||||
events={events}
|
||||
onEventClick={handleEventClickFromDay}
|
||||
onCreateEvent={handleCreateEventFromDay}
|
||||
/>
|
||||
|
||||
{/* Модальное окно просмотра деталей события */}
|
||||
<ViewEventDetailsModal
|
||||
isOpen={showEventDetailsModal}
|
||||
onClose={() => {
|
||||
setShowEventDetailsModal(false);
|
||||
setViewingEvent(null);
|
||||
}}
|
||||
event={viewingEvent}
|
||||
onEdit={handleEditFromDetails}
|
||||
onDelete={async () => {
|
||||
if (!viewingEvent?.id) return;
|
||||
if (!confirm("Удалить это событие?")) return;
|
||||
|
||||
try {
|
||||
await apiClient.deleteEvent(viewingEvent.id);
|
||||
setShowEventDetailsModal(false);
|
||||
setViewingEvent(null);
|
||||
loadEvents();
|
||||
} catch (err) {
|
||||
console.error("Ошибка удаления события:", err);
|
||||
alert("Не удалось удалить событие");
|
||||
}
|
||||
}}
|
||||
showParticipants={true}
|
||||
/>
|
||||
|
||||
{/* Модальное окно управления календарем */}
|
||||
<CalendarModal
|
||||
isOpen={showCalendarModal}
|
||||
|
||||
136
frontend/src/components/ViewDayEventsModal.tsx
Normal file
136
frontend/src/components/ViewDayEventsModal.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import { X, Plus, Clock, Calendar } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { ru } from "date-fns/locale";
|
||||
|
||||
interface ViewDayEventsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
date: Date | null;
|
||||
events: any[];
|
||||
onEventClick: (event: any) => void;
|
||||
onCreateEvent: () => void;
|
||||
}
|
||||
|
||||
export function ViewDayEventsModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
date,
|
||||
events,
|
||||
onEventClick,
|
||||
onCreateEvent,
|
||||
}: ViewDayEventsModalProps) {
|
||||
if (!isOpen || !date) return null;
|
||||
|
||||
const formattedDate = format(date, "d MMMM yyyy, EEEE", { locale: ru });
|
||||
const capitalizedDate = formattedDate.charAt(0).toUpperCase() + formattedDate.slice(1);
|
||||
|
||||
const dayEvents = events.filter(event => {
|
||||
const eventDate = new Date(event.start);
|
||||
return eventDate.toDateString() === date.toDateString();
|
||||
});
|
||||
|
||||
// Сортируем события по времени начала
|
||||
const sortedEvents = [...dayEvents].sort((a, b) => {
|
||||
return new Date(a.start).getTime() - new Date(b.start).getTime();
|
||||
});
|
||||
|
||||
const formatTime = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return format(date, "HH:mm", { locale: ru });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="w-full max-w-2xl rounded-2xl bg-white shadow-xl max-h-[85vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">События на {capitalizedDate}</h3>
|
||||
<p className="text-sm text-gray-500 mt-0.5">
|
||||
{sortedEvents.length === 0
|
||||
? "Нет событий"
|
||||
: `${sortedEvents.length} ${sortedEvents.length === 1 ? "событие" : "событий"}`}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-full p-1 hover:bg-gray-100 transition"
|
||||
>
|
||||
<X size={20} className="text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Events List */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
{sortedEvents.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Calendar size={48} className="text-gray-300 mb-3" />
|
||||
<p className="text-gray-500 text-sm mb-1">Нет событий в этот день</p>
|
||||
<p className="text-gray-400 text-xs">Создайте новое событие, нажав кнопку ниже</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{sortedEvents.map((event) => (
|
||||
<button
|
||||
key={event.id}
|
||||
onClick={() => onEventClick(event)}
|
||||
className="w-full text-left rounded-lg border border-gray-200 bg-white p-4 transition hover:border-sky-300 hover:bg-sky-50/50 hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Color Indicator */}
|
||||
<div
|
||||
className="mt-1 h-10 w-1 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: event.color_event || "#3498db" }}
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Title with recurring indicator */}
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-medium text-gray-900 truncate">
|
||||
{event.title}
|
||||
</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>
|
||||
|
||||
{/* Time */}
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600">
|
||||
<Clock size={14} />
|
||||
<span>
|
||||
{formatTime(event.start)} - {formatTime(event.end)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Description preview */}
|
||||
{event.description && (
|
||||
<p className="text-xs text-gray-500 mt-2 line-clamp-2">
|
||||
{event.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-gray-200 px-6 py-4">
|
||||
<button
|
||||
onClick={onCreateEvent}
|
||||
className="w-full flex items-center justify-center gap-2 rounded-lg bg-sky-500 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-sky-600"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Создать событие на этот день
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
204
frontend/src/components/ViewEventDetailsModal.tsx
Normal file
204
frontend/src/components/ViewEventDetailsModal.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import { X, Edit2, Trash2, Clock, Calendar, FileText, Users } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { ru } from "date-fns/locale";
|
||||
import { useState, useEffect } from "react";
|
||||
import { apiClient } from "@/lib/api";
|
||||
|
||||
interface ViewEventDetailsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
event: any | null;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
showParticipants?: boolean;
|
||||
}
|
||||
|
||||
export function ViewEventDetailsModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
event,
|
||||
onEdit,
|
||||
onDelete,
|
||||
showParticipants = false,
|
||||
}: ViewEventDetailsModalProps) {
|
||||
const [participants, setParticipants] = useState<any[]>([]);
|
||||
const [loadingParticipants, setLoadingParticipants] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && event?.id && showParticipants) {
|
||||
loadParticipants();
|
||||
} else {
|
||||
setParticipants([]);
|
||||
}
|
||||
}, [isOpen, event?.id, showParticipants]);
|
||||
|
||||
const loadParticipants = async () => {
|
||||
if (!event?.id) return;
|
||||
|
||||
try {
|
||||
setLoadingParticipants(true);
|
||||
const result = await apiClient.getEventParticipants(event.id);
|
||||
const participantsList = Array.isArray(result) ? result : (result?.results || []);
|
||||
setParticipants(participantsList);
|
||||
} catch (error) {
|
||||
console.error("Failed to load participants:", error);
|
||||
} finally {
|
||||
setLoadingParticipants(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen || !event) return null;
|
||||
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return format(date, "d MMMM yyyy, HH:mm", { locale: ru });
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return format(date, "d MMMM yyyy", { locale: ru });
|
||||
};
|
||||
|
||||
const capitalizedStart = formatDateTime(event.start).charAt(0).toUpperCase() + formatDateTime(event.start).slice(1);
|
||||
const capitalizedEnd = formatDateTime(event.end).charAt(0).toUpperCase() + formatDateTime(event.end).slice(1);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="w-full max-w-2xl rounded-2xl bg-white shadow-xl max-h-[85vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between border-b border-gray-200 px-6 py-4">
|
||||
<div className="flex-1 min-w-0 pr-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 truncate">
|
||||
{event.title}
|
||||
</h3>
|
||||
{event.rule && (
|
||||
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded flex-shrink-0">
|
||||
🔁 Повторяется
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-500">
|
||||
<div
|
||||
className="h-3 w-3 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: event.color_event || "#3498db" }}
|
||||
/>
|
||||
<span className="truncate">Календарь #{event.calendar}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-full p-1 hover:bg-gray-100 transition flex-shrink-0"
|
||||
>
|
||||
<X size={20} className="text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
|
||||
{/* Time */}
|
||||
<div className="flex items-start gap-3">
|
||||
<Clock size={20} className="text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-700">Время</p>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
<span className="font-medium">Начало:</span> {capitalizedStart}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 mt-0.5">
|
||||
<span className="font-medium">Конец:</span> {capitalizedEnd}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recurring Info */}
|
||||
{event.rule && event.end_recurring_period && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Calendar size={20} className="text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-700">Повторение</p>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
До {formatDate(event.end_recurring_period)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{event.description && (
|
||||
<div className="flex items-start gap-3">
|
||||
<FileText size={20} className="text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">Описание</p>
|
||||
<p className="text-sm text-gray-600 whitespace-pre-wrap">
|
||||
{event.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Participants */}
|
||||
{showParticipants && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Users size={20} className="text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">
|
||||
Участники {participants.length > 0 && `(${participants.length})`}
|
||||
</p>
|
||||
{loadingParticipants ? (
|
||||
<p className="text-xs text-gray-500">Загрузка...</p>
|
||||
) : participants.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{participants.map((participant: any) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-2"
|
||||
>
|
||||
<div className="h-7 w-7 rounded-full bg-sky-100 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-xs font-medium text-sky-700">
|
||||
{participant.user_name?.[0] || "?"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900 truncate">
|
||||
{participant.user_name}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
{participant.distinction || "attendee"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">Нет участников</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-gray-200 px-6 py-4">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-sky-500 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-sky-600"
|
||||
>
|
||||
<Edit2 size={16} />
|
||||
Редактировать
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="rounded-lg border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600 transition hover:bg-red-100"
|
||||
title="Удалить событие"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user