"use client"; import { Bell, Building2, CalendarDays, ChevronDown, FileSignature, FileText, Home as HomeIcon, Menu, MessageSquare, Monitor, Search, ShoppingCart, Users, Wallet, X, Sparkles } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { ReactNode, useEffect, useRef, useState, useCallback, useMemo } from "react"; import { apiClient } from "@/lib/api"; import { useUser } from "@/contexts/UserContext"; import { useNotifications } from "@/hooks/useApi"; import { getVerbCategory } from "@/lib/verbTranslations"; import { CalendarModal } from "@/components/CalendarModal"; import CalendarParticipantsModal from "@/components/CalendarParticipantsModal"; import { NotificationCenter, NotificationPanel } from "@/components/NotificationCenter"; import { EventModal } from "@/components/EventModal"; import { ViewDayEventsModal } from "@/components/ViewDayEventsModal"; import { ViewEventDetailsModal } from "@/components/ViewEventDetailsModal"; import { CalendarSidebar } from "@/components/calendar/CalendarSidebar"; import { CalendarCard } from "@/components/calendar/CalendarCard"; import type { CalendarEvent } from "@/services/calendarService"; type AppShellProps = { children: ReactNode; }; type PageHeaderProps = { title: string; subtitle?: string; badge?: string; eyebrow?: string; }; type HeaderProps = { onOpenLeftNav: () => void; onOpenCalendar: () => void; }; type LeftNavContentProps = { onNavigate?: () => void; }; const navItems = [ { href: "/", label: "Лента", icon: HomeIcon, category: "Новости" }, { href: "/messages", label: "Сообщения", icon: MessageSquare, category: "Сообщения" }, { href: "/employees", label: "Сотрудники", icon: Users }, { href: "/departments", label: "Отделы", icon: Building2 }, { href: "/requests", label: "Заявления", icon: FileSignature, category: "Заявки" }, { href: "/equipment", label: "Оборудование", icon: Monitor }, { href: "/procurement", label: "Закупки", icon: ShoppingCart, category: "Закупки" }, { href: "/documents", label: "Документы", icon: FileText, category: "Документы" }, { href: "/finances", label: "Финансы", icon: Wallet }, ]; function Header({ onOpenLeftNav, onOpenCalendar }: HeaderProps) { const router = useRouter(); const { user, logout } = useUser(); const [searchQuery, setSearchQuery] = useState(""); const [userMenuOpen, setUserMenuOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false); const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); const searchInputRef = useRef(null); // Закрытие меню при клике вне useEffect(() => { if (!userMenuOpen) return; const handler = (e: MouseEvent) => { const target = e.target as Node; const desktop = document.getElementById("user-menu-root"); const mobile = document.getElementById("user-menu-root-mobile"); const insideDesktop = desktop && desktop.contains(target); const insideMobile = mobile && mobile.contains(target); if (!insideDesktop && !insideMobile) { setUserMenuOpen(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [userMenuOpen]); // Закрытие панели уведомлений при клике вне useEffect(() => { if (!isNotificationsOpen) return; const handler = (e: MouseEvent) => { const target = e.target as Node; const notificationButton = document.querySelector('[aria-label="Уведомления"]'); const notificationPanel = document.querySelector('.notification-panel-mobile'); const insideButton = notificationButton && notificationButton.contains(target); const insidePanel = notificationPanel && notificationPanel.contains(target); if (!insideButton && !insidePanel) { setIsNotificationsOpen(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [isNotificationsOpen]); const userInitials = user ? `${user.last_name?.[0] || ''}${user.first_name?.[0] || ''}` : 'Г'; const userName = user ? `${user.last_name} ${user.first_name}`.trim() : 'Гость'; const submitSearch = () => { const query = searchQuery.trim(); if (!query) return; router.push(`/search?q=${encodeURIComponent(query)}`); }; return (
Логотип
{ setIsNotificationsOpen((v) => !v); setIsSearchOpen(false); }} />
setSearchQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); submitSearch(); } }} className="w-full rounded-full border border-gray-200 bg-gray-50 py-2.5 pl-11 pr-4 text-sm text-gray-800 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100" placeholder="Поиск" />
setUserMenuOpen((v) => !v)} > {user?.avatar ? ( {userName} ) : ( userInitials )}
{/* Меню пользователя */} {userMenuOpen && (
)}
{/* Мобильная строка поиска */}
setSearchQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); submitSearch(); setIsSearchOpen(false); } if (e.key === "Escape") { setIsSearchOpen(false); } }} className="w-full rounded-full border border-gray-200 bg-gray-50 py-2.5 pl-11 pr-4 text-sm text-gray-800 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100" placeholder="Поиск по сайту..." />
{/* Мобильная панель уведомлений */}
{isNotificationsOpen && ( setIsNotificationsOpen(false)} /> )}
); } function LeftNavContent({ onNavigate }: LeftNavContentProps) { const pathname = usePathname(); const { notifications: notificationsData, markCategoryAsRead } = useNotifications(); const notifications = Array.isArray(notificationsData) ? notificationsData : []; // Подсчет уведомлений по категориям const categoryCounts = useMemo(() => { const counts: Record = {}; notifications.forEach((n: any) => { // Пропускаем прочитанные if (!n.verb || n.is_read) return; const category = getVerbCategory(n.verb); counts[category] = (counts[category] || 0) + 1; }); return counts; }, [notifications]); const handleNavClick = async (category?: string) => { // Помечаем уведомления категории как прочитанные if (category && categoryCounts[category] > 0) { await markCategoryAsRead(category); } // Вызываем callback для закрытия мобильного меню if (onNavigate) { onNavigate(); } }; const navLinkClass = (href: string) => `flex items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-50 ${pathname === href ? "bg-sky-50 text-sky-700 ring-1 ring-sky-100" : "text-gray-700" }`; const navIconClass = (href: string) => (pathname === href ? "text-sky-700" : "text-gray-400"); return (
{navItems.map(({ href, label, icon: Icon, category }) => { const count = category ? categoryCounts[category] || 0 : 0; return ( handleNavClick(category)} > {label} {count > 0 && ( {count > 99 ? '99+' : count} )} ); })}
); } function LeftNav() { return ( ); } export function PageHeader({ title, subtitle, badge, eyebrow = "Раздел" }: PageHeaderProps) { return (

{eyebrow}

{title}

{badge ? (
{badge}
) : null}
{subtitle ?

{subtitle}

: null}
); } export function AppShell({ children }: AppShellProps) { const { user, loading, logout } = useUser(); const router = useRouter(); const pathname = usePathname(); const isMessageDialogPage = pathname.startsWith('/messages/') && pathname !== '/messages' && !pathname.includes('/settings'); // Вычисляем данные пользователя один раз на уровне AppShell const userInitials = user ? `${user.last_name?.[0] || ''}${user.first_name?.[0] || ''}` : 'Г'; const userName = user ? `${user.last_name} ${user.first_name}`.trim() : 'Гость'; const [isMobileLeftNavOpen, setIsMobileLeftNavOpen] = useState(false); const [isMobileCalendarOpen, setIsMobileCalendarOpen] = useState(false); const [isProfileExpanded, setIsProfileExpanded] = useState(false); // Состояния модалов календаря const [showCalendarModal, setShowCalendarModal] = useState(false); const [showEventModal, setShowEventModal] = useState(false); const [showParticipantsModal, setShowParticipantsModal] = useState(false); const [editingCalendar, setEditingCalendar] = useState<{ id?: number; name: string } | null>(null); const [editingEvent, setEditingEvent] = useState(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(null); const [showEventDetailsModal, setShowEventDetailsModal] = useState(false); const [viewingEvent, setViewingEvent] = useState(null); const [sidebarEvents, setSidebarEvents] = useState([]); const [currentSelectedCalendarId, setCurrentSelectedCalendarId] = useState(null); // Мемоизированные callback'и для предотвращения ререндеров CalendarCard const handleSetSidebarEvents = useCallback((events: CalendarEvent[]) => { setSidebarEvents(events); }, []); const handleCalendarChange = useCallback((calendarId: number | null) => { setCurrentSelectedCalendarId(calendarId); }, []); const handleSetEventsRefreshTrigger = useCallback((value: number | ((prev: number) => number)) => { setEventsRefreshTrigger(value); }, []); // Обработчики модалов (мемоизированные для предотвращения ререндеров) const handleOpenCalendarModal = useCallback((calendar?: { id?: number; name: string }) => { setEditingCalendar(calendar || { name: "" }); setShowCalendarModal(true); }, []); const handleOpenEventModal = useCallback((event: any, date?: Date) => { // Если передана дата (клик на дату), открываем модал просмотра дня 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 (!currentSelectedCalendarId) { alert("Сначала выберите календарь"); return; } 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: currentSelectedCalendarId, color_event: "#3498db", }); setShowDayEventsModal(false); setShowEventModal(true); }, [currentSelectedCalendarId, 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); }, []); const handleOpenParticipantsModal = useCallback((calendar: { id: number; name: string; user_role?: string }) => { setParticipantsCalendar(calendar); setShowParticipantsModal(true); }, []); useEffect(() => { // Если загрузка завершена и пользователь не авторизован - редирект на логин if (!loading && !user) { router.push('/login'); } }, [user, loading, router]); useEffect(() => { setIsMobileLeftNavOpen(false); setIsMobileCalendarOpen(false); }, [pathname]); useEffect(() => { const onResize = () => { if (window.innerWidth >= 1024) { setIsMobileLeftNavOpen(false); setIsMobileCalendarOpen(false); } }; window.addEventListener("resize", onResize); onResize(); return () => { window.removeEventListener("resize", onResize); }; }, []); useEffect(() => { if (isMobileLeftNavOpen || isMobileCalendarOpen) { document.body.style.overflow = "hidden"; return; } document.body.style.overflow = ""; return () => { document.body.style.overflow = ""; }; }, [isMobileLeftNavOpen, isMobileCalendarOpen]); // Показываем загрузку, пока проверяем авторизацию if (loading) { return (

Загрузка...

); } // Если пользователь не авторизован, ничего не показываем (уже редиректим) if (!user) { return null; } return (
setIsMobileLeftNavOpen(true)} onOpenCalendar={() => setIsMobileCalendarOpen(true)} />
{children}
{/* Профиль пользователя */}
{isProfileExpanded && (
)}
setIsMobileLeftNavOpen(false)} />
{isMobileCalendarOpen && ( )} {/* Модалы календаря - рендерятся на верхнем уровне */} { setShowCalendarModal(false); setEditingCalendar(null); }} calendar={editingCalendar} /> {/* Модальное окно участников календаря */} {participantsCalendar && ( { setShowParticipantsModal(false); setParticipantsCalendar(null); }} calendarId={participantsCalendar.id} calendarName={participantsCalendar.name} userRole={participantsCalendar.user_role} /> )} {/* Модальное окно события */} { setShowEventModal(false); setEditingEvent(null); }} event={editingEvent} onSave={handleEventSaved} showParticipants={true} /> {/* Модальное окно просмотра событий дня */} { setShowDayEventsModal(false); setSelectedDateForModal(null); }} date={selectedDateForModal} events={sidebarEvents} onEventClick={handleEventClickFromDay} onCreateEvent={handleCreateEventFromDay} /> {/* Модальное окно просмотра деталей события */} { 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} /> ); }