From cb6d89dff2d967e538ffa3385670115ac1eaddc3 Mon Sep 17 00:00:00 2001 From: igor_nadein Date: Thu, 19 Feb 2026 14:17:20 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20UI=20=D1=81=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=BE=D0=BC=20AppShell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен компонент AppShell с Header, Sidebar, Calendar - Установлен lucide-react для современных иконок - Обновлены все страницы: главная, login, employees, documents, etc - Единый дизайн с PageHeader и переиспользуемыми элементами - Улучшенная навигация с активными ссылками - Responsive design с sticky элементами - Градиентный фон и современные shadow/ring эффекты --- frontend/package-lock.json | 13 + frontend/src/app/departments/page.tsx | 11 +- frontend/src/app/documents/page.tsx | 11 +- frontend/src/app/employees/page.tsx | 247 +++---------------- frontend/src/app/finances/page.tsx | 11 +- frontend/src/app/login/page.tsx | 93 +++----- frontend/src/app/messages/page.tsx | 11 +- frontend/src/app/page.tsx | 330 +++++++------------------- frontend/src/app/register/page.tsx | 203 ++-------------- frontend/src/app/requests/page.tsx | 11 +- frontend/src/components/AppShell.tsx | 173 ++++++++++++++ 11 files changed, 383 insertions(+), 731 deletions(-) create mode 100644 frontend/src/components/AppShell.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ce159998..23e44ed7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -64,6 +64,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1460,6 +1461,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1515,6 +1517,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -1995,6 +1998,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2312,6 +2316,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2841,6 +2846,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3018,6 +3024,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4540,6 +4547,7 @@ "version": "0.574.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.574.0.tgz", "integrity": "sha512-dJ8xb5juiZVIbdSn3HTyHsjjIwUwZ4FNwV0RtYDScOyySOeie1oXZTymST6YPJ4Qwt3Po8g4quhYl4OxtACiuQ==", + "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -5074,6 +5082,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5082,6 +5091,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5727,6 +5737,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -5878,6 +5889,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6138,6 +6150,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/app/departments/page.tsx b/frontend/src/app/departments/page.tsx index eac2f5b4..fe8d4191 100644 --- a/frontend/src/app/departments/page.tsx +++ b/frontend/src/app/departments/page.tsx @@ -1,10 +1,9 @@ +import { AppShell, PageHeader } from "../../components/AppShell"; + export default function DepartmentsPage() { return ( -
-
-

Отделы

-

Раздел в разработке. Здесь будут списки отделов и контакты.

-
-
+ + + ); } diff --git a/frontend/src/app/documents/page.tsx b/frontend/src/app/documents/page.tsx index 47754e31..600c97b4 100644 --- a/frontend/src/app/documents/page.tsx +++ b/frontend/src/app/documents/page.tsx @@ -1,10 +1,9 @@ +import { AppShell, PageHeader } from "../../components/AppShell"; + export default function DocumentsPage() { return ( -
-
-

Документы

-

Раздел в разработке. Здесь будут договоры, отчёты и файлы.

-
-
+ + + ); } diff --git a/frontend/src/app/employees/page.tsx b/frontend/src/app/employees/page.tsx index c297083b..c80975c5 100644 --- a/frontend/src/app/employees/page.tsx +++ b/frontend/src/app/employees/page.tsx @@ -1,222 +1,47 @@ -"use client"; +import { AppShell, PageHeader } from "../../components/AppShell"; -import { Bell, Building2, FileSignature, FileText, Home as HomeIcon, MessageSquare, Search, Users, Wallet } from "lucide-react"; -import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; -import { useEmployees, useCurrentUser } from "@/hooks/useApi"; -import { useState, useEffect } from "react"; -import { apiClient } from "@/lib/api"; - -const weekdays = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]; - -const calendarWeeks = [ - ["", "", "", "", "1", "2", "3"], - ["4", "5", "6", "7", "8", "9", "10"], - ["11", "12", "13", "14", "15", "16", "17"], - ["18", "19", "20", "21", "22", "23", "24"], - ["25", "26", "27", "28", "", "", ""], +const employees = [ + { id: 1, name: "Константин Макаев", role: "Руководитель проектов", status: "Онлайн", department: "ИТ" }, + { id: 2, name: "Виктория Зацарина", role: "Дизайнер", status: "Недавно", department: "Маркетинг" }, + { id: 3, name: "Алексей Пономарёв", role: "Разработчик", status: "Оффлайн", department: "ИТ" }, + { id: 4, name: "Эдуард Иванов", role: "Директор", status: "Онлайн", department: "Финансы" }, ]; - export default function EmployeesPage() { - const pathname = usePathname(); - const router = useRouter(); - const [searchQuery, setSearchQuery] = useState(""); - const { employees, loading, error } = useEmployees({ search: searchQuery }); - const { user } = useCurrentUser(); - - // Проверка авторизации - useEffect(() => { - const token = apiClient.getToken(); - if (!token) { - router.push('/login'); - } - }, [router]); - - const sortedEmployees = employees.sort((a, b) => - `${a.first_name} ${a.last_name}`.localeCompare(`${b.first_name} ${b.last_name}`, "ru") - ); - - 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"); + const sortedEmployees = [...employees].sort((a, b) => a.name.localeCompare(b.name, "ru")); return ( -
-
-
-
- Логотип -
-
-
- - setSearchQuery(e.target.value)} - 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="Поиск сотрудников..." - /> -
-
-
- -
КМ
-
-
-
+ + -
- - -
-
-
-
-

Команда

-

Сотрудники

-
-
Всего: {employees.length}
-
-
- -
- {loading && ( -
-

Загрузка сотрудников...

-
- )} - {error && ( -
-

Ошибка загрузки: {error.message}

-
- )} - {!loading && !error && employees.length === 0 && ( -
-

Сотрудники не найдены

-
- )} - {!loading && sortedEmployees.map((employee) => { - const isOnline = employee.is_active; - const statusText = isOnline ? "Онлайн" : "Оффлайн"; - const statusColor = isOnline ? "text-green-600" : "text-gray-500"; - const dotColor = isOnline ? "bg-green-500" : "bg-gray-400"; - const fullName = `${employee.first_name} ${employee.last_name}`; - - return ( -
-
- {employee.first_name[0]}{employee.last_name[0]} -
-
-

{fullName}

-

{employee.position?.name || 'Не указана'}

-
- - {statusText} - {employee.department && ( - <> - • - {employee.department.name} - - )} -
-
-
- ); - })} -
-
- - + + ); + })}
-
+ ); } diff --git a/frontend/src/app/finances/page.tsx b/frontend/src/app/finances/page.tsx index 82c2c713..3dfb42af 100644 --- a/frontend/src/app/finances/page.tsx +++ b/frontend/src/app/finances/page.tsx @@ -1,10 +1,9 @@ +import { AppShell, PageHeader } from "../../components/AppShell"; + export default function FinancesPage() { return ( -
-
-

Финансы

-

Раздел в разработке. Здесь появится аналитика и отчётность.

-
-
+ + + ); } diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 226f4d17..eef6e319 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -1,69 +1,29 @@ -'use client'; - -import { useState, FormEvent } from 'react'; -import { useRouter } from 'next/navigation'; -import { apiClient } from '@/lib/api'; +import Link from "next/link"; export default function Login() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - const router = useRouter(); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setError(''); - setLoading(true); - - try { - const response = await apiClient.login({ email, password }); - console.log('Успешная авторизация:', response.user); - - // Перенаправляем на главную страницу - router.push('/'); - router.refresh(); // Обновляем данные - } catch (err) { - console.error('Ошибка авторизации:', err); - setError('Неверный email или пароль'); - } finally { - setLoading(false); - } - }; - return (

- Войти в систему EUSRR + Войти в аккаунт

- Используйте учетные данные LDAP для входа + Используйте телефон или почту, чтобы продолжить.

-
- {error && ( -
- {error} -
- )} - +
-
@@ -75,34 +35,37 @@ export default function Login() { name="password" type="password" autoComplete="current-password" - value={password} - onChange={(e) => setPassword(e.target.value)} className="w-full rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-900 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100" placeholder="••••••••" - required - disabled={loading} />
+
+ + + Забыли пароль? + +
-
- -
- Создать аккаунт - -
- + +

- Для входа используйте корпоративные учетные данные + Продолжая, вы соглашаетесь с условиями использования и подтверждаете ознакомление с политикой конфиденциальности.

diff --git a/frontend/src/app/messages/page.tsx b/frontend/src/app/messages/page.tsx index f932cb59..e601431f 100644 --- a/frontend/src/app/messages/page.tsx +++ b/frontend/src/app/messages/page.tsx @@ -1,10 +1,9 @@ +import { AppShell, PageHeader } from "../../components/AppShell"; + export default function MessagesPage() { return ( -
-
-

Сообщения

-

Раздел в разработке. Здесь появятся чаты и диалоги.

-
-
+ + + ); } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 240937e4..697972d6 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,259 +1,93 @@ -"use client"; +import { Heart, MessageSquare, Search, Star } from "lucide-react"; +import { AppShell } from "../components/AppShell"; -import { Bell, Bookmark, Building2, FileSignature, FileText, Heart, HeartHandshake, Home as HomeIcon, MessageSquare, Search, Star, Users, Wallet } from "lucide-react"; -import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; -import { usePosts, useCurrentUser } from "@/hooks/useApi"; -import { useEffect } from "react"; -import { apiClient } from "@/lib/api"; - -function formatTimeAgo(dateString: string): string { - const date = new Date(dateString); - const now = new Date(); - const diffInMs = now.getTime() - date.getTime(); - const diffInMinutes = Math.floor(diffInMs / (1000 * 60)); - const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); - const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); - - if (diffInMinutes < 60) return `${diffInMinutes} минут назад`; - if (diffInHours < 24) return `${diffInHours} часов назад`; - return `${diffInDays} дней назад`; -} - -const weekdays = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]; - -const calendarWeeks = [ - ["", "", "", "", "1", "2", "3"], - ["4", "5", "6", "7", "8", "9", "10"], - ["11", "12", "13", "14", "15", "16", "17"], - ["18", "19", "20", "21", "22", "23", "24"], - ["25", "26", "27", "28", "", "", ""], +const feedItems = [ + { + id: 1, + author: "Алексей Пономарёв", + time: "2 часа назад", + text: "Обновил подборку треков для фокуса. Делюсь плейлистом, если тоже любите работать под легкий электро." , + tags: ["Музыка", "Работа"], + actions: { likes: 42, comments: 8, saves: 5 }, + }, + { + id: 2, + author: "Виктория Зацарина", + time: "3 часа назад", + text: "Собрали командой фотографии с последней пробежки в парке. Город оживает, когда выходит солнце." , + tags: ["Спорт", "Фото"], + actions: { likes: 65, comments: 12, saves: 7 }, + }, + { + id: 3, + author: "Unity C#", + time: "5 часов назад", + text: "Сегодня делимся лучшими гайдами по микроанимациям в интерфейсах. Собрали 7 примеров с кодом и видео." , + tags: ["Дизайн", "Guides"], + actions: { likes: 88, comments: 16, saves: 21 }, + }, ]; export default function Home() { - const pathname = usePathname(); - const router = useRouter(); - const { posts, loading, error } = usePosts(); - const { user } = useCurrentUser(); - - // Проверка авторизации при загрузке - useEffect(() => { - const token = apiClient.getToken(); - if (!token) { - router.push('/login'); - } - }, [router]); - - 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 ( -
-
-
-
- Логотип +
+
+
КМ
+
+ + - -
-
-
- - -
-
- -
- -
- КМ -
-
- -
- - -
-
-
-
- КМ -
-
- - -
-
-
- Статья - Фото - Ссылка - Опрос - Событие -
-
- -
- {loading && ( -
-

Загрузка...

-
- )} - {error && ( -
-

Ошибка загрузки: {error.message}

-

Проверьте подключение к серверу

-
- )} - {!loading && !error && posts.length === 0 && ( -
-

Нет постов в ленте

-
- )} - {!loading && posts.map((post) => ( -
-
-
-
- {post.author.first_name[0]}{post.author.last_name[0]} -
-
-

{post.author.first_name} {post.author.last_name}

-

{formatTimeAgo(post.created_at)}

-
-
-
-

{post.content}

- {post.tags && post.tags.length > 0 && ( -
- {post.tags.map((tag) => ( - - #{tag} - - ))} -
- )} -
- - - -
-
- ))} -
-
- - +
+ Статья + Фото + Ссылка + Опрос + Событие +
-
+ +
+ {feedItems.map((item) => ( +
+
+
+
+ {item.author[0]} +
+
+

{item.author}

+

{item.time}

+
+
+
+

{item.text}

+
+ {item.tags.map((tag) => ( + + {tag} + + ))} +
+
+ + + +
+
+ ))} +
+ ); } diff --git a/frontend/src/app/register/page.tsx b/frontend/src/app/register/page.tsx index 11ff9405..788e8f32 100644 --- a/frontend/src/app/register/page.tsx +++ b/frontend/src/app/register/page.tsx @@ -1,79 +1,6 @@ -'use client'; - -import { useState, FormEvent } from 'react'; -import { useRouter } from 'next/navigation'; -import { apiClient } from '@/lib/api'; import Link from "next/link"; export default function Register() { - const [formData, setFormData] = useState({ - first_name: '', - last_name: '', - middle_name: '', - email: '', - phone_number: '', - telegram: '', - password: '', - confirmPassword: '', - }); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - const router = useRouter(); - - const handleChange = (e: React.ChangeEvent) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value, - }); - }; - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setError(''); - - // Валидация - if (formData.password !== formData.confirmPassword) { - setError('Пароли не совпадают'); - return; - } - - if (!formData.email && !formData.phone_number && !formData.telegram) { - setError('Укажите хотя бы один способ связи: email, телефон или Telegram'); - return; - } - - setLoading(true); - - try { - const registerData = { - email: formData.email, - phone_number: formData.phone_number, - password: formData.password, - first_name: formData.first_name, - last_name: formData.last_name, - middle_name: formData.middle_name || undefined, - telegram: formData.telegram || undefined, - }; - - await apiClient.register(registerData); - - // После успешной регистрации авторизуемся - const loginData = { - email: formData.email, - phone_number: formData.phone_number, - password: formData.password - }; - await apiClient.login(loginData); - - router.push('/'); - } catch (err: any) { - console.error('Ошибка регистрации:', err); - setError(err.message || 'Ошибка при регистрации. Проверьте введенные данные.'); - } finally { - setLoading(false); - } - }; - return (
@@ -85,173 +12,95 @@ export default function Register() { Создайте новый аккаунт, указав имя и контактные данные.

-
- {error && ( -
- {error} -
- )} - +
-
-
-
-
- -
- -
- -
- - -
- -
- - -
-
- -
- Обязательные поля: Имя, Фамилия, Пароль
- Контакты: Укажите хотя бы один (Email, Телефон или Telegram) -
+ -
- -
Уже есть аккаунт? Войти -
- +

Регистрируясь, вы принимаете условия использования и подтверждаете ознакомление с политикой конфиденциальности.

diff --git a/frontend/src/app/requests/page.tsx b/frontend/src/app/requests/page.tsx index b9e1443c..23994468 100644 --- a/frontend/src/app/requests/page.tsx +++ b/frontend/src/app/requests/page.tsx @@ -1,10 +1,9 @@ +import { AppShell, PageHeader } from "../../components/AppShell"; + export default function RequestsPage() { return ( -
-
-

Заявления

-

Раздел в разработке. Здесь появятся заявки и согласования.

-
-
+ + + ); } diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx new file mode 100644 index 00000000..42b5e4b2 --- /dev/null +++ b/frontend/src/components/AppShell.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { Bell, Building2, FileSignature, FileText, Home as HomeIcon, MessageSquare, Search, Users, Wallet } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ReactNode } from "react"; + +type AppShellProps = { + children: ReactNode; +}; + +type PageHeaderProps = { + title: string; + subtitle?: string; + badge?: string; + eyebrow?: string; +}; + +const navItems = [ + { href: "/", label: "Лента", icon: HomeIcon }, + { href: "/messages", label: "Сообщения", icon: MessageSquare }, + { href: "/employees", label: "Сотрудники", icon: Users }, + { href: "/departments", label: "Отделы", icon: Building2 }, + { href: "/requests", label: "Заявления", icon: FileSignature }, + { href: "/documents", label: "Документы", icon: FileText }, + { href: "/finances", label: "Финансы", icon: Wallet }, +]; + +const weekdays = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]; + +const calendarWeeks = [ + ["", "", "", "", "1", "2", "3"], + ["4", "5", "6", "7", "8", "9", "10"], + ["11", "12", "13", "14", "15", "16", "17"], + ["18", "19", "20", "21", "22", "23", "24"], + ["25", "26", "27", "28", "", "", ""], +]; + +function Header() { + return ( +
+
+ + Логотип + +
+
+ + +
+
+
+ +
КМ
+
+
+
+ ); +} + +function LeftNav() { + const pathname = usePathname(); + + 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 ( + + ); +} + +function Calendar() { + return ( + + ); +} + +export function PageHeader({ title, subtitle, badge, eyebrow = "Раздел" }: PageHeaderProps) { + return ( +
+
+
+

{eyebrow}

+

{title}

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

{subtitle}

: null} +
+ ); +} + +export function AppShell({ children }: AppShellProps) { + return ( +
+
+
+ +
{children}
+ +
+
+ ); +}