From c5470e01317e1d8f1155a93916cc8bebd6cbfbfb Mon Sep 17 00:00:00 2001 From: igor_nadein Date: Fri, 20 Feb 2026 11:08:19 +0300 Subject: [PATCH] Clean up corp.front directory and update frontend layout/AppShell --- corp.front/.dockerignore | 35 -- corp.front/.env.example | 5 - corp.front/Dockerfile | 48 --- corp.front/INTEGRATION_GUIDE.md | 410 ---------------------- corp.front/next.config.ts | 14 - corp.front/src/app/employees/page.tsx | 220 ------------ corp.front/src/app/login/page.tsx | 111 ------ corp.front/src/app/page.tsx | 257 -------------- corp.front/src/app/register/page.tsx | 262 -------------- corp.front/src/hooks/useApi.ts | 218 ------------ corp.front/src/types/api.ts | 164 --------- frontend/src/app/layout.tsx | 4 +- frontend/src/components/AppShell.tsx | 475 ++++++++++++++++++++------ 13 files changed, 379 insertions(+), 1844 deletions(-) delete mode 100644 corp.front/.dockerignore delete mode 100644 corp.front/.env.example delete mode 100644 corp.front/Dockerfile delete mode 100644 corp.front/INTEGRATION_GUIDE.md delete mode 100644 corp.front/next.config.ts delete mode 100644 corp.front/src/app/employees/page.tsx delete mode 100644 corp.front/src/app/login/page.tsx delete mode 100644 corp.front/src/app/page.tsx delete mode 100644 corp.front/src/app/register/page.tsx delete mode 100644 corp.front/src/hooks/useApi.ts delete mode 100644 corp.front/src/types/api.ts diff --git a/corp.front/.dockerignore b/corp.front/.dockerignore deleted file mode 100644 index 13e59b48..00000000 --- a/corp.front/.dockerignore +++ /dev/null @@ -1,35 +0,0 @@ -# Dependencies -node_modules -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Next.js -.next/ -out/ -build -dist - -# Environment -.env*.local -.env - -# Testing -coverage - -# Misc -.DS_Store -*.pem - -# Debug -*.log - -# IDE -.vscode -.idea -*.swp -*.swo -*~ - -# Vercel -.vercel diff --git a/corp.front/.env.example b/corp.front/.env.example deleted file mode 100644 index f0f5164d..00000000 --- a/corp.front/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Backend API URL -NEXT_PUBLIC_API_URL=http://localhost:8000 - -# WebSocket URL for real-time features -NEXT_PUBLIC_WS_URL=ws://localhost:8000 diff --git a/corp.front/Dockerfile b/corp.front/Dockerfile deleted file mode 100644 index 75ed2dd4..00000000 --- a/corp.front/Dockerfile +++ /dev/null @@ -1,48 +0,0 @@ -# Build stage -FROM node:20-alpine AS builder - -WORKDIR /app - -# Copy package files -COPY package.json package-lock.json* ./ -RUN npm ci - -# Copy source files -COPY . . - -# Set environment variables for build -ENV NEXT_TELEMETRY_DISABLED=1 -ENV NODE_ENV=production - -# Build the application -RUN npm run build - -# Production stage -FROM node:20-alpine AS runner - -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -# Copy necessary files from builder -COPY --from=builder /app/public ./public -COPY --from=builder /app/.next ./.next -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/package.json ./package.json - -# Set correct permissions -RUN chown -R nextjs:nodejs /app - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" - -# Start Next.js server -CMD ["npm", "start"] diff --git a/corp.front/INTEGRATION_GUIDE.md b/corp.front/INTEGRATION_GUIDE.md deleted file mode 100644 index 31c6835d..00000000 --- a/corp.front/INTEGRATION_GUIDE.md +++ /dev/null @@ -1,410 +0,0 @@ -# Frontend Integration Guide - -## Обзор - -Фронтенд corp.front полностью интегрирован с Django Backend через REST API. Используется Next.js 16 с TypeScript и Tailwind CSS. - -## Структура проекта - -``` -corp.front/ -├── src/ -│ ├── app/ # Next.js страницы -│ │ ├── page.tsx # Главная (Лента) ✅ Интегрирован -│ │ ├── employees/ # Сотрудники ✅ Интегрирован -│ │ ├── departments/ # Отделы -│ │ ├── documents/ # Документы -│ │ ├── messages/ # Сообщения -│ │ ├── requests/ # Заявки -│ │ ├── finances/ # Финансы -│ │ └── login/ # Вход -│ ├── lib/ -│ │ └── api.ts # API клиент ✅ -│ ├── types/ -│ │ └── api.ts # TypeScript типы ✅ -│ └── hooks/ -│ └── useApi.ts # React хуки для API ✅ -``` - -## API клиент - -### Базовая конфигурация - -API клиент автоматически: -- Добавляет JWT токен к запросам -- Обрабатывает ошибки авторизации -- Сохраняет токены в localStorage -- Перенаправляет на /login при 401 - -### Использование - -```typescript -import apiClient from '@/lib/api'; - -// Авторизация -const { access, refresh, user } = await apiClient.login({ - username: 'user@example.com', - password: 'password' -}); - -// Получение данных -const employees = await apiClient.getEmployees(); -const posts = await apiClient.getPosts(); -``` - -## React хуки - -Используйте готовые хуки из `@/hooks/useApi`: - -```tsx -import { useCurrentUser, useEmployees, usePosts } from '@/hooks/useApi'; - -function MyComponent() { - const { user, loading, error } = useCurrentUser(); - const { employees } = useEmployees({ search: 'John' }); - const { posts } = usePosts({ page: 1 }); - - if (loading) return
Загрузка...
; - if (error) return
Ошибка: {error.message}
; - - return
{/* UI */}
; -} -``` - -### Доступные хуки - -- `useCurrentUser()` - текущий пользователь -- `useEmployees(params?)` - список сотрудников -- `useDepartments()` - список отделов -- `usePosts(params?)` - посты ленты -- `useDocuments(params?)` - документы -- `useRequests(params?)` - заявки -- `useChats()` - чаты -- `useNotifications()` - уведомления - -## TypeScript типы - -Все типы данных определены в `@/types/api.ts`: - -```typescript -import type { User, Post, Department, Document } from '@/types/api'; -``` - -## Интегрированные страницы - -### ✅ Главная страница (Лента) - -**Файл**: `src/app/page.tsx` - -**Функции**: -- Загрузка постов из API -- Отображение автора, времени, контента -- Лайки и комментарии -- Теги постов - -**Используемые хуки**: -- `usePosts()` - получение постов -- `useCurrentUser()` - текущий пользователь - -### ✅ Сотрудники - -**Файл**: `src/app/employees/page.tsx` - -**Функции**: -- Список всех сотрудников -- Поиск по имени -- Статус (онлайн/оффлайн) -- Должность и отдел - -**Используемые хуки**: -- `useEmployees({ search })` - поиск сотрудников -- `useCurrentUser()` - текущий пользователь - -## Переменные окружения - -### Для локальной разработки - -`.env.local`: -```env -NEXT_PUBLIC_API_URL=http://localhost:8000 -NEXT_PUBLIC_WS_URL=ws://localhost:8000 -``` - -### Для Docker - -`.env.local` (автоматически используется в docker-compose): -```env -NEXT_PUBLIC_API_URL=http://web:8000 -NEXT_PUBLIC_WS_URL=ws://web:8000 -``` - -## Запуск - -### Локальная разработка - -```bash -cd corp.front -npm install -npm run dev -``` - -Доступно на: http://localhost:3000 - -### Docker - -```bash -# Из корня проекта -docker-compose up -d frontend -``` - -Доступно через nginx: http://localhost - -## Интеграция новых страниц - -### Шаг 1: Создать страницу - -```tsx -// src/app/mypage/page.tsx -"use client"; - -import { useMyData } from '@/hooks/useApi'; - -export default function MyPage() { - const { data, loading, error } = useMyData(); - - if (loading) return
Загрузка...
; - if (error) return
Ошибка: {error.message}
; - - return ( -
- {data.map(item => ( -
{item.name}
- ))} -
- ); -} -``` - -### Шаг 2: Добавить метод в API клиент - -```typescript -// src/lib/api.ts -class ApiClient { - // ... existing methods - - async getMyData() { - return this.request('/mydata/'); - } -} -``` - -### Шаг 3: Создать хук (опционально) - -```typescript -// src/hooks/useApi.ts -export function useMyData() { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function fetchData() { - try { - const result = await apiClient.getMyData(); - setData(result); - } catch (err) { - setError(err); - } finally { - setLoading(false); - } - } - fetchData(); - }, []); - - return { data, loading, error }; -} -``` - -## API Endpoints - -Полный список доступных эндпоинтов: - -### Auth -- `POST /api/auth/token/` - Login -- `POST /api/auth/token/refresh/` - Refresh token -- `POST /api/v1/auth/register/` - Регистрация - -### Employees -- `GET /api/v1/employees/` - Список сотрудников -- `GET /api/v1/employees/{id}/` - Детали сотрудника -- `GET /api/v1/employees/me/` - Текущий пользователь - -### Departments -- `GET /api/v1/departments/` - Список отделов -- `GET /api/v1/departments/{id}/` - Детали отдела - -### Feed -- `GET /api/v1/posts/` - Посты ленты -- `POST /api/v1/posts/` - Создать пост -- `POST /api/v1/posts/{id}/like/` - Лайк поста -- `GET /api/v1/comments/` - Комментарии -- `POST /api/v1/comments/` - Создать комментарий - -### Documents -- `GET /api/v1/documents/` - Документы -- `GET /api/v1/documents/{id}/` - Детали документа - -### Requests -- `GET /api/v1/requests/` - Заявки -- `POST /api/v1/requests/` - Создать заявку -- `GET /api/v1/requests/{id}/` - Детали заявки - -### Communications -- `GET /api/v1/communications/chats/` - Чаты -- `GET /api/v1/communications/messages/` - Сообщения -- `POST /api/v1/communications/messages/` - Отправить сообщение - -### Calendar -- `GET /api/v1/calendar/events/` - События календаря -- `GET /api/v1/calendar/calendars/` - Календари - -### Notifications -- `GET /api/v1/notifications/` - Уведомления -- `POST /api/v1/notifications/{id}/mark_read/` - Отметить прочитанным -- `POST /api/v1/notifications/mark_all_read/` - Отметить все прочитанными - -## Аутентификация - -### Вход - -```typescript -import apiClient from '@/lib/api'; - -async function handleLogin(username: string, password: string) { - try { - const { access, refresh, user } = await apiClient.login({ - username, - password - }); - - // Токен автоматически сохранён - // Перенаправить пользователя - router.push('/'); - } catch (error) { - console.error('Login failed:', error); - } -} -``` - -### Выход - -```typescript -import apiClient from '@/lib/api'; - -function handleLogout() { - apiClient.clearToken(); - router.push('/login'); -} -``` - -### Защищённые страницы - -```tsx -"use client"; - -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import apiClient from '@/lib/api'; - -export default function ProtectedPage() { - const router = useRouter(); - - useEffect(() => { - const token = apiClient.getToken(); - if (!token) { - router.push('/login'); - } - }, []); - - return
Protected content
; -} -``` - -## Обработка ошибок - -API клиент автоматически обрабатывает: - -- **401 Unauthorized**: Очищает токены и перенаправляет на `/login` -- **Network errors**: Выбрасывает исключение с сообщением - -Пример обработки в компонентах: - -```tsx -const { data, loading, error } = useMyData(); - -if (loading) return ; - -if (error) { - return ( -
-

Ошибка загрузки данных

-

{error.message}

- -
- ); -} -``` - -## WebSocket (для будущей реализации) - -Для real-time обновлений можно добавить WebSocket соединение: - -```typescript -const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000'; - -const socket = new WebSocket(`${WS_URL}/ws/notifications/`); - -socket.onmessage = (event) => { - const data = JSON.parse(event.data); - // Обработать уведомление -}; -``` - -## Рекомендации - -1. **Всегда используйте хуки** для получения данных из API -2. **Обрабатывайте loading и error** состояния в UI -3. **Используйte TypeScript типы** для type-safety -4. **Не сохраняйте чувствительные данные** в localStorage кроме токенов -5. **Валидируйте данные** перед отправкой на сервер - -## Troubleshooting - -###CORS ошибки - -Убедитесь, что backend настроен с правильными CORS заголовками: - -```python -# backend/eusrr_backend/settings.py -CORS_ALLOWED_ORIGINS = [ - "http://localhost:3000", - "http://localhost", -] -``` - -### 401 Unauthorized - -- Проверьте, что токен действителен -- Убедитесь, что backend запущен -- Проверьте переменные окружения - -### Ошибка сети - -- Проверьте, что backend доступен -- Проверьте URL в `NEXT_PUBLIC_API_URL` -- Проверьте docker network, если используете docker-compose - ---- - -**Дата создания**: 19 февраля 2026 -**Версия**: 1.0 diff --git a/corp.front/next.config.ts b/corp.front/next.config.ts deleted file mode 100644 index 1d2435c6..00000000 --- a/corp.front/next.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - // Rewrites не нужны - в production используйте nginx (http://localhost) - // Для dev можно запустить backend локально на localhost:8000 - - // Environment variables - env: { - NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, - NEXT_PUBLIC_WS_URL: process.env.NEXT_PUBLIC_WS_URL, - }, -}; - -export default nextConfig; diff --git a/corp.front/src/app/employees/page.tsx b/corp.front/src/app/employees/page.tsx deleted file mode 100644 index be6aacfc..00000000 --- a/corp.front/src/app/employees/page.tsx +++ /dev/null @@ -1,220 +0,0 @@ -"use client"; - -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", "", "", ""], -]; - -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"); - - 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/corp.front/src/app/login/page.tsx b/corp.front/src/app/login/page.tsx deleted file mode 100644 index 69387fa2..00000000 --- a/corp.front/src/app/login/page.tsx +++ /dev/null @@ -1,111 +0,0 @@ -'use client'; - -import { useState, FormEvent } from 'react'; -import { useRouter } from 'next/navigation'; -import { apiClient } from '@/lib/api'; - -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} -
- )} - -
- - setEmail(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="example@company.com или +79001234567" - required - disabled={loading} - /> -
-
- - 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/corp.front/src/app/page.tsx b/corp.front/src/app/page.tsx deleted file mode 100644 index b6401d00..00000000 --- a/corp.front/src/app/page.tsx +++ /dev/null @@ -1,257 +0,0 @@ -"use client"; - -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", "", "", ""], -]; - -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} - - ))} -
- )} -
- - - -
-
- ))} -
-
- - -
-
- ); -} diff --git a/corp.front/src/app/register/page.tsx b/corp.front/src/app/register/page.tsx deleted file mode 100644 index 3e46b971..00000000 --- a/corp.front/src/app/register/page.tsx +++ /dev/null @@ -1,262 +0,0 @@ -'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 ( -
-
-
-

- Регистрация -

-

- Создайте новый аккаунт, указав имя и контактные данные. -

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

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

-
-
-
- ); -} diff --git a/corp.front/src/hooks/useApi.ts b/corp.front/src/hooks/useApi.ts deleted file mode 100644 index 7e814b56..00000000 --- a/corp.front/src/hooks/useApi.ts +++ /dev/null @@ -1,218 +0,0 @@ -"use client"; - -import { useEffect, useState } from 'react'; -import apiClient from '@/lib/api'; -import type { User, Post, PaginatedResponse } from '@/types/api'; - -export function useCurrentUser() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function fetchUser() { - try { - const data = await apiClient.getCurrentUser(); - setUser(data); - } catch (err) { - setError(err as Error); - } finally { - setLoading(false); - } - } - - fetchUser(); - }, []); - - return { user, loading, error }; -} - -export function useEmployees(params?: { search?: string; department?: string }) { - const [employees, setEmployees] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function fetchEmployees() { - try { - setLoading(true); - const data = await apiClient.getEmployees(params); - setEmployees(data.results || data); - } catch (err) { - setError(err as Error); - } finally { - setLoading(false); - } - } - - fetchEmployees(); - }, [params?.search, params?.department]); - - return { employees, loading, error, refetch: () => { } }; -} - -export function useDepartments() { - const [departments, setDepartments] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function fetchDepartments() { - try { - const data = await apiClient.getDepartments(); - setDepartments(data.results || data); - } catch (err) { - setError(err as Error); - } finally { - setLoading(false); - } - } - - fetchDepartments(); - }, []); - - return { departments, loading, error }; -} - -export function usePosts(params?: { page?: number; search?: string }) { - const [posts, setPosts] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [hasMore, setHasMore] = useState(false); - - useEffect(() => { - async function fetchPosts() { - try { - setLoading(true); - const data: PaginatedResponse = await apiClient.getPosts(params); - setPosts(data.results); - setHasMore(!!data.next); - } catch (err) { - setError(err as Error); - } finally { - setLoading(false); - } - } - - fetchPosts(); - }, [params?.page, params?.search]); - - return { posts, loading, error, hasMore, refetch: () => { } }; -} - -export function useDocuments(params?: { search?: string; type?: string }) { - const [documents, setDocuments] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function fetchDocuments() { - try { - const data = await apiClient.getDocuments(params); - setDocuments(data.results || data); - } catch (err) { - setError(err as Error); - } finally { - setLoading(false); - } - } - - fetchDocuments(); - }, [params?.search, params?.type]); - - return { documents, loading, error }; -} - -export function useRequests(params?: { status?: string; type?: string }) { - const [requests, setRequests] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function fetchRequests() { - try { - const data = await apiClient.getRequests(params); - setRequests(data.results || data); - } catch (err) { - setError(err as Error); - } finally { - setLoading(false); - } - } - - fetchRequests(); - }, [params?.status, params?.type]); - - return { requests, loading, error }; -} - -export function useChats() { - const [chats, setChats] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function fetchChats() { - try { - const data = await apiClient.getChats(); - setChats(data.results || data); - } catch (err) { - setError(err as Error); - } finally { - setLoading(false); - } - } - - fetchChats(); - }, []); - - return { chats, loading, error }; -} - -export function useNotifications() { - const [notifications, setNotifications] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [unreadCount, setUnreadCount] = useState(0); - - useEffect(() => { - async function fetchNotifications() { - try { - const data = await apiClient.getNotifications(); - const notifs = data.results || data; - setNotifications(notifs); - setUnreadCount(notifs.filter((n: any) => !n.is_read).length); - } catch (err) { - setError(err as Error); - } finally { - setLoading(false); - } - } - - fetchNotifications(); - }, []); - - const markAsRead = async (id: number) => { - try { - await apiClient.markNotificationAsRead(id); - setNotifications(prev => - prev.map(n => (n.id === id ? { ...n, is_read: true } : n)) - ); - setUnreadCount(prev => Math.max(0, prev - 1)); - } catch (err) { - console.error('Failed to mark notification as read:', err); - } - }; - - const markAllAsRead = async () => { - try { - await apiClient.markAllNotificationsAsRead(); - setNotifications(prev => prev.map(n => ({ ...n, is_read: true }))); - setUnreadCount(0); - } catch (err) { - console.error('Failed to mark all notifications as read:', err); - } - }; - - return { notifications, loading, error, unreadCount, markAsRead, markAllAsRead }; -} diff --git a/corp.front/src/types/api.ts b/corp.front/src/types/api.ts deleted file mode 100644 index e1acd0a4..00000000 --- a/corp.front/src/types/api.ts +++ /dev/null @@ -1,164 +0,0 @@ -// Common types -export interface User { - id: number; - email: string; - phone_number?: string; - first_name: string; - last_name: string; - middle_name?: string; - position?: Position; - department?: Department; - avatar?: string; - is_active: boolean; - telegram?: string; -} - -export interface Department { - id: number; - name: string; - description?: string; - parent?: number; - head?: User; - employees_count?: number; -} - -export interface Position { - id: number; - name: string; - description?: string; -} - -// Feed types -export interface Post { - id: number; - author: User; - content: string; - tags?: string[]; - created_at: string; - updated_at: string; - likes_count: number; - comments_count: number; - is_liked?: boolean; -} - -export interface Comment { - id: number; - post: number; - author: User; - content: string; - created_at: string; - updated_at: string; -} - -// Documents types -export interface Document { - id: number; - title: string; - description?: string; - file?: string; - document_type: string; - created_by: User; - created_at: string; - updated_at: string; - tags?: string[]; -} - -// Requests types -export interface Request { - id: number; - title: string; - description: string; - request_type: string; - status: 'pending' | 'approved' | 'rejected' | 'in_progress' | 'completed'; - created_by: User; - assigned_to?: User; - created_at: string; - updated_at: string; -} - -// Communications types -export interface Chat { - id: number; - name?: string; - chat_type: 'direct' | 'group' | 'department'; - participants: User[]; - last_message?: Message; - unread_count?: number; - created_at: string; -} - -export interface Message { - id: number; - chat: number; - sender: User; - content: string; - is_read: boolean; - created_at: string; -} - -// Calendar types -export interface CalendarEvent { - id: number; - title: string; - description?: string; - start_time: string; - end_time: string; - location?: string; - calendar: number; - participants?: User[]; - created_by: User; -} - -export interface Calendar { - id: number; - name: string; - description?: string; - color?: string; - is_public: boolean; - owner: User; -} - -// Notifications types -export interface Notification { - id: number; - title: string; - message: string; - notification_type: string; - is_read: boolean; - created_at: string; - link?: string; -} - -// API Response types -export interface PaginatedResponse { - count: number; - next: string | null; - previous: string | null; - results: T[]; -} - -// Auth types -export interface LoginCredentials { - email?: string; - phone?: string; - phone_number?: string; - password: string; -} - -export interface LoginResponse { - access: string; - refresh: string; - user: User; -} - -export interface RegisterData { - email: string; - phone_number: string; - password: string; - first_name: string; - last_name: string; - middle_name?: string; - telegram?: string; - whatsapp?: string; - wechat?: string; -} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index c7ee372c..50ab629b 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -14,8 +14,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "EUSRR - Корпоративная система", + description: "Корпоративная система управления EUSRR", }; export default function RootLayout({ diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index 8360d352..ccb0d453 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -1,9 +1,23 @@ "use client"; -import { Bell, Building2, FileSignature, FileText, Home as HomeIcon, MessageSquare, Search, Users, Wallet } from "lucide-react"; +import { + Bell, + Building2, + CalendarDays, + FileSignature, + FileText, + Home as HomeIcon, + LogOut, + Menu, + MessageSquare, + Search, + Users, + Wallet, + X, +} from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; -import { ReactNode, useEffect } from "react"; +import { ReactNode, useEffect, useRef, useState } from "react"; import { useUser } from "@/contexts/UserContext"; type AppShellProps = { @@ -17,6 +31,15 @@ type PageHeaderProps = { eyebrow?: string; }; +type HeaderProps = { + onOpenLeftNav: () => void; + onOpenCalendar: () => void; +}; + +type LeftNavContentProps = { + onNavigate?: () => void; +}; + const navItems = [ { href: "/", label: "Лента", icon: HomeIcon }, { href: "/messages", label: "Сообщения", icon: MessageSquare }, @@ -37,9 +60,64 @@ const calendarWeeks = [ ["25", "26", "27", "28", "", "", ""], ]; -function Header() { +function Header({ onOpenLeftNav, onOpenCalendar }: HeaderProps) { + const router = useRouter(); const { user, logout } = useUser(); + const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); + const [isDesktopProfileMenuOpen, setIsDesktopProfileMenuOpen] = useState(false); + const [isMobileProfileMenuOpen, setIsMobileProfileMenuOpen] = useState(false); + + const notificationsRef = useRef(null); + const desktopProfileRef = useRef(null); + const mobileProfileRef = useRef(null); + + useEffect(() => { + const onClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + + const clickedOutsideNotifications = notificationsRef.current && !notificationsRef.current.contains(target); + const clickedOutsideDesktopProfile = desktopProfileRef.current && !desktopProfileRef.current.contains(target); + const clickedOutsideMobileProfile = mobileProfileRef.current && !mobileProfileRef.current.contains(target); + + if (clickedOutsideNotifications) { + setIsNotificationsOpen(false); + } + + if (clickedOutsideDesktopProfile) { + setIsDesktopProfileMenuOpen(false); + } + + if (clickedOutsideMobileProfile) { + setIsMobileProfileMenuOpen(false); + } + }; + + const onEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsNotificationsOpen(false); + setIsDesktopProfileMenuOpen(false); + setIsMobileProfileMenuOpen(false); + } + }; + + document.addEventListener("mousedown", onClickOutside); + document.addEventListener("keydown", onEscape); + + return () => { + document.removeEventListener("mousedown", onClickOutside); + document.removeEventListener("keydown", onEscape); + }; + }, []); + + const handleLogout = () => { + setIsNotificationsOpen(false); + setIsDesktopProfileMenuOpen(false); + setIsMobileProfileMenuOpen(false); + logout(); + router.push("/login"); + }; + const userInitials = user ? `${user.last_name?.[0] || ''}${user.first_name?.[0] || ''}` : 'Г'; @@ -48,39 +126,148 @@ function Header() { : 'Гость'; return ( -
-
- - Логотип - -
-
- - +
+
+
+ + Логотип + + +
+
+ + +
+
+ +
+
+ + + {isNotificationsOpen ? ( +
+ новых уведомлений нет +
+ ) : null} +
+ +
+ + + {isDesktopProfileMenuOpen ? ( +
+ +
+ ) : null} +
-
- -
{ - if (confirm('Выйти из системы?')) { - logout(); - } - }} - > - {user?.avatar ? ( - {userName} - ) : ( - userInitials - )} + +
+
+ + + + Логотип + + + +
+
+ +
+
+
+ + +
+ +
+ + + {isMobileProfileMenuOpen ? ( +
+ +
+ ) : null} +
@@ -88,10 +275,17 @@ function Header() { ); } -function LeftNav() { +function LeftNavContent({ onNavigate }: LeftNavContentProps) { const pathname = usePathname(); const { user } = useUser(); + 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 userInitials = user ? `${user.last_name?.[0] || ''}${user.first_name?.[0] || ''}` : 'Г'; @@ -99,80 +293,87 @@ function LeftNav() { ? `${user.last_name} ${user.first_name}`.trim() : 'Гость'; - 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 ( +
+
+
+ {user?.avatar ? ( + {userName} + ) : ( + userInitials + )} +
+
+

{userName}

+

{user?.is_active ? 'Онлайн' : 'Оффлайн'}

+
+
+
+ {navItems.map(({ href, label, icon: Icon }) => ( + + + {label} + + ))} +
+
+ ); +} +function LeftNav() { return ( ); } +function CalendarCard() { + return ( +
+
+ Календарь + Февраль 2026 +
+
+ {weekdays.map((day) => ( +
+ {day} +
+ ))} +
+
+ {calendarWeeks.map((week, wi) => + week.map((day, di) => { + const isToday = day === "18"; + return ( +
+ {day} +
+ ); + }) + )} +
+
+ ); +} + function Calendar() { return ( ); @@ -198,6 +399,10 @@ export function PageHeader({ title, subtitle, badge, eyebrow = "Раздел" }: export function AppShell({ children }: AppShellProps) { const { user, loading } = useUser(); const router = useRouter(); + const pathname = usePathname(); + + const [isMobileLeftNavOpen, setIsMobileLeftNavOpen] = useState(false); + const [isMobileCalendarOpen, setIsMobileCalendarOpen] = useState(false); useEffect(() => { // Если загрузка завершена и пользователь не авторизован - редирект на логин @@ -206,6 +411,24 @@ export function AppShell({ children }: AppShellProps) { } }, [user, loading, router]); + useEffect(() => { + setIsMobileLeftNavOpen(false); + setIsMobileCalendarOpen(false); + }, [pathname]); + + useEffect(() => { + if (isMobileLeftNavOpen || isMobileCalendarOpen) { + document.body.style.overflow = "hidden"; + return; + } + + document.body.style.overflow = ""; + + return () => { + document.body.style.overflow = ""; + }; + }, [isMobileLeftNavOpen, isMobileCalendarOpen]); + // Показываем загрузку, пока проверяем авторизацию if (loading) { return ( @@ -225,12 +448,68 @@ export function AppShell({ children }: AppShellProps) { return (
-
-
+
setIsMobileLeftNavOpen(true)} onOpenCalendar={() => setIsMobileCalendarOpen(true)} /> +
{children}
+ + {/* Мобильное левое меню */} +
+ +
+ setIsMobileLeftNavOpen(false)} /> +
+
+ + {/* Мобильный календарь */} +
+ +
+ +
+
); }