"use client"; import { Building2, CalendarDays, Download, FileSignature, FileText, Home as HomeIcon, Menu, MessageSquare, Monitor, Search, ShoppingCart, Users } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { ReactNode, startTransition, useEffect, useRef, useState, useMemo } from "react"; import { useMobileNavPlacement } from "@/contexts/MobileNavPlacementContext"; import { useUser } from "@/contexts/UserContext"; import { useNotifications } from "@/hooks/useApi"; import { getVerbCategory } from "@/lib/verbTranslations"; import { NotificationCenter, NotificationPanel } from "@/components/NotificationCenter"; import { CalendarSidebar } from "@/components/calendar/CalendarSidebar"; import { PushOnboardingPrompt } from "@/components/PushOnboardingPrompt"; import { useCalendarModals } from "@/hooks/useCalendarModals"; import { CalendarModals } from "@/components/layout/CalendarModals"; import { MobileLeftDrawer, MobileCalendarDrawer } from "@/components/layout/MobileDrawers"; import { usePwa } from "@/contexts/PwaContext"; type AppShellProps = { children: ReactNode; }; type PageHeaderProps = { title: string; subtitle?: string; badge?: string; eyebrow?: string; }; type HeaderProps = { mobileNavPlacement: "top" | "bottom"; suppressMobileChrome?: boolean; 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({ mobileNavPlacement, suppressMobileChrome = false, onOpenLeftNav, onOpenCalendar }: HeaderProps) { const router = useRouter(); const { user, logout } = useUser(); const { canInstall, install } = usePwa(); 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 isBottomMobileNav = mobileNavPlacement === "bottom"; const submitSearch = () => { const query = searchQuery.trim(); if (!query) return; router.push(`/search?q=${encodeURIComponent(query)}`); }; return (
{/* eslint-disable-next-line @next/next/no-img-element */} Логотип
{ setIsNotificationsOpen((v) => !v); setIsSearchOpen(false); }} />
{ e.preventDefault(); submitSearch(); }} > setSearchQuery(e.target.value)} enterKeyHint="search" onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); submitSearch(); } }} className="app-input w-full rounded-full py-2.5 pl-11 pr-4 text-sm" placeholder="Поиск" />
setUserMenuOpen((v) => !v)} > {user?.avatar ? ( /* eslint-disable-next-line @next/next/no-img-element */ {userName} ) : ( userInitials )}
{/* Меню пользователя */} {userMenuOpen && (
{canInstall ? ( ) : null}
)}
{/* Мобильная строка поиска */}
{ e.preventDefault(); submitSearch(); setIsSearchOpen(false); }} > setSearchQuery(e.target.value)} enterKeyHint="search" onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); submitSearch(); setIsSearchOpen(false); } if (e.key === "Escape") { setIsSearchOpen(false); } }} className="app-input w-full rounded-full py-2.5 pl-11 pr-12 text-sm" placeholder="Поиск по сайту..." />
{/* Мобильная панель уведомлений */}
{isNotificationsOpen && (
setIsNotificationsOpen(false)} />
)}
); } function LeftNavContent({ onNavigate }: LeftNavContentProps) { const pathname = usePathname(); const { notifications: notificationsData, markCategoryAsRead } = useNotifications(); const notifications = useMemo(() => Array.isArray(notificationsData) ? notificationsData : [], [notificationsData]); // Подсчет уведомлений по категориям const categoryCounts = useMemo(() => { const counts: Record = {}; notifications.forEach((n: { verb?: string; is_read?: boolean }) => { // Пропускаем прочитанные 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 && 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 transition ${pathname === href ? "app-selected" : "text-[var(--foreground)] hover:bg-[var(--surface-secondary)]" }`; const navIconClass = (href: string) => (pathname === href ? "app-accent-text" : "app-text-muted"); 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({ fixedDesktop = false }: { fixedDesktop?: boolean }) { 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 } = useUser(); const { mobileNavPlacement } = useMobileNavPlacement(); const router = useRouter(); const pathname = usePathname(); const isMessageDialogPage = pathname.startsWith('/messages/') && pathname !== '/messages' && !pathname.includes('/settings'); const [isMobileLeftNavOpen, setIsMobileLeftNavOpen] = useState(false); const [isMobileCalendarOpen, setIsMobileCalendarOpen] = useState(false); const prevPathnameRef = useRef(pathname); const cal = useCalendarModals(); useEffect(() => { // Если загрузка завершена и пользователь не авторизован - редирект на логин if (!loading && !user) { router.push('/login'); } }, [user, loading, router]); // Close mobile drawers on route change useEffect(() => { if (prevPathnameRef.current !== pathname) { prevPathnameRef.current = pathname; startTransition(() => { 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)} />
{!isMessageDialogPage ? : null} {children}
setIsMobileLeftNavOpen(false)} > setIsMobileLeftNavOpen(false)} /> setIsMobileCalendarOpen(false)} onOpenCalendarModal={cal.handleOpenCalendarModal} onOpenEventModal={cal.handleOpenEventModal} onOpenParticipantsModal={cal.handleOpenParticipantsModal} eventsRefreshTrigger={cal.eventsRefreshTrigger} setEventsRefreshTrigger={cal.handleSetEventsRefreshTrigger} setSidebarEvents={cal.handleSetSidebarEvents} onCalendarChange={cal.handleCalendarChange} />
); }