From cc1f9d84738b1e757e191c35bcca7b7fd426289f Mon Sep 17 00:00:00 2001 From: Bivekich Date: Sun, 20 Jul 2025 22:50:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=20=D1=81=20=D1=82=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=BC=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=BF=D0=BE=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D0=B5=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=81=D0=BE=D1=82=D1=80=D1=83=D0=B4=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20=D0=B2=20package.json=20=D0=B8=20pac?= =?UTF-8?q?kage-lock.json,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BF=D0=B0=D0=BA?= =?UTF-8?q?=D0=B5=D1=82=20react-resizable-panels.=20=D0=92=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=B5=D0=BD=D1=8B=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D1=83?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D0=B1=D0=BE=D0=BA=D0=BE=D0=B2=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D0=B8=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=B0=D1=82.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=B0=D1=82=20=D0=B2=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=20DateTime=20=D0=B2=20GraphQL.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EMPLOYEE_TEST_TASK.md | 379 ------------------ package-lock.json | 11 + package.json | 1 + src/app/providers.tsx | 5 +- src/components/admin/categories-section.tsx | 18 +- src/components/admin/users-section.tsx | 22 +- src/components/dashboard/sidebar.tsx | 178 +++++--- src/components/dashboard/user-settings.tsx | 4 +- src/components/market/market-dashboard.tsx | 4 +- .../messenger/messenger-dashboard.tsx | 235 +++-------- .../partners/partners-dashboard.tsx | 4 +- .../services/services-dashboard.tsx | 4 +- .../warehouse/warehouse-dashboard.tsx | 12 +- src/graphql/resolvers.ts | 33 +- src/graphql/typedefs.ts | 94 ++--- src/hooks/useSidebar.tsx | 46 +++ 16 files changed, 354 insertions(+), 696 deletions(-) delete mode 100644 EMPLOYEE_TEST_TASK.md create mode 100644 src/hooks/useSidebar.tsx diff --git a/EMPLOYEE_TEST_TASK.md b/EMPLOYEE_TEST_TASK.md deleted file mode 100644 index 146b7b5..0000000 --- a/EMPLOYEE_TEST_TASK.md +++ /dev/null @@ -1,379 +0,0 @@ -# Тестовое задание: Система управления сотрудниками - -## Описание задачи - -Необходимо разработать с нуля полнофункциональную систему управления сотрудниками компании. Система должна позволять добавлять, редактировать, удалять сотрудников, а также управлять их расписанием и просматривать статистику. - -## Технический стек - -- **Frontend**: Next.js 15+ (App Router) -- **Backend**: GraphQL с Apollo Server -- **Database**: PostgreSQL с Prisma ORM -- **Styling**: TailwindCSS с glassmorphism эффектами -- **UI Components**: Radix UI (через shadcn/ui) -- **TypeScript**: строгая типизация -- **Icons**: Lucide React -- **Notifications**: Sonner - -## Дизайн-система и стили - -### Цветовая схема -- Основные цвета: оттенки фиолетового (oklch(0.75 0.32 315) до oklch(0.68 0.28 280)) -- Фон: тёмный градиент `bg-gradient-smooth` -- Карточки: стеклянный эффект `glass-card` -- Текст: белый и оттенки белого/серого - -### Glassmorphism стили -```css -.glass-card { - background: rgba(255, 255, 255, 0.12); - backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: - 0 8px 32px rgba(168, 85, 247, 0.18), - 0 4px 16px rgba(147, 51, 234, 0.12), - inset 0 1px 0 rgba(255, 255, 255, 0.3); -} - -.glass-card:hover { - background: rgba(255, 255, 255, 0.15); - border: 1px solid rgba(255, 255, 255, 0.3); - box-shadow: - 0 12px 40px rgba(168, 85, 247, 0.25), - 0 6px 20px rgba(147, 51, 234, 0.18); -} -``` - -### Анимации -- Плавные переходы (0.3s ease) -- Hover-эффекты с изменением opacity и shadow -- Анимация появления форм -- Интерактивные элементы с transform - -## Структура базы данных - -```prisma -model Employee { - id String @id @default(cuid()) - firstName String - lastName String - middleName String? - birthDate DateTime? - avatar String? - passportPhoto String? - passportSeries String? - passportNumber String? - passportIssued String? - passportDate DateTime? - address String? - position String - department String? - hireDate DateTime - salary Float? - status EmployeeStatus @default(ACTIVE) - phone String - email String? - telegram String? - whatsapp String? - emergencyContact String? - emergencyPhone String? - organizationId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - scheduleRecords EmployeeSchedule[] - organization Organization @relation(fields: [organizationId], references: [id]) -} - -model EmployeeSchedule { - id String @id @default(cuid()) - date DateTime - status ScheduleStatus - hoursWorked Float? - notes String? - employeeId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - employee Employee @relation(fields: [employeeId], references: [id]) -} - -enum EmployeeStatus { - ACTIVE - VACATION - SICK - FIRED -} - -enum ScheduleStatus { - WORK - WEEKEND - VACATION - SICK - ABSENT -} -``` - -## Функциональные требования - -### 1. Основная страница сотрудников (/employees) - -**Макет страницы:** -- Sidebar слева (аналогично dashboard) -- Основной контент справа -- Поиск по ФИО, телефону, должности -- Табы: "Сотрудники", "Расписание", "Статистика" - -**Таб "Сотрудники":** -- Карточки сотрудников в grid layout (3-4 в ряд) -- Каждая карточка содержит: - - Аватар (или инициалы, если нет фото) - - ФИО - - Должность - - Телефон - - Email (если есть) - - Статус (бейдж с цветом) - - Кнопки: "Редактировать", "Уволить" -- Кнопка "Добавить сотрудника" вверху -- Фильтры по статусу -- Пагинация при большом количестве - -### 2. Добавление сотрудника - -**Inline форма (появляется как первая карточка):** -- Обязательные поля: - - Имя, Фамилия - - Должность - - Дата приёма - - Телефон -- Необязательные поля: - - Отчество - - Email - - Дата рождения - - Адрес - - Оклад - - Паспортные данные - - Контакты для экстренной связи - - Telegram/WhatsApp -- Загрузка аватара -- Валидация всех полей -- Красивые маски ввода (телефон, паспорт, ЗП) - -### 3. Редактирование сотрудника - -**Inline редактирование:** -- Форма заменяет карточку сотрудника -- Все те же поля, что при создании -- Предзаполненные данные -- Возможность изменить статус -- Кнопки "Сохранить" / "Отмена" - -### 4. Управление расписанием - -**Таб "Расписание":** -- Календарь на месяц -- Выбор сотрудника из dropdown -- Отметки на каждый день: - - Работа (зелёный) - - Выходной (серый) - - Отпуск (синий) - - Больничный (жёлтый) - - Прогул (красный) -- Возможность массово отметить период -- Подсчёт отработанных часов/дней - -### 5. Статистика - -**Таб "Статистика":** -- Общее количество сотрудников -- Распределение по статусам -- Средний возраст -- Средняя зарплата -- График найма по месяцам -- Топ-должности -- Статистика посещаемости - -## GraphQL API - -### Queries -```graphql -type Query { - employees: [Employee!]! - employee(id: ID!): Employee - employeeSchedule(employeeId: ID!, year: Int!, month: Int!): [EmployeeSchedule!]! - employeeStats: EmployeeStats! -} -``` - -### Mutations -```graphql -type Mutation { - createEmployee(input: CreateEmployeeInput!): CreateEmployeeResponse! - updateEmployee(id: ID!, input: UpdateEmployeeInput!): UpdateEmployeeResponse! - deleteEmployee(id: ID!): Boolean! - updateEmployeeSchedule(input: UpdateScheduleInput!): Boolean! -} -``` - -### Types -```graphql -type Employee { - id: ID! - firstName: String! - lastName: String! - middleName: String - birthDate: DateTime - avatar: String - position: String! - department: String - hireDate: DateTime! - salary: Float - status: EmployeeStatus! - phone: String! - email: String - telegram: String - whatsapp: String - emergencyContact: String - emergencyPhone: String - createdAt: DateTime! - updatedAt: DateTime! - scheduleRecords: [EmployeeSchedule!]! -} -``` - -## Требования к реализации - -### 1. Файловая структура -``` -src/ -├── app/ -│ └── employees/ -│ └── page.tsx -├── components/ -│ └── employees/ -│ ├── employees-dashboard.tsx -│ ├── employee-card.tsx -│ ├── employee-form.tsx -│ ├── employee-schedule.tsx -│ └── employee-stats.tsx -├── graphql/ -│ ├── queries.ts -│ ├── mutations.ts -│ ├── typedefs.ts -│ └── resolvers.ts -└── lib/ - ├── validations.ts - └── input-masks.ts -``` - -### 2. Компоненты - -**EmployeesDashboard** - основной контейнер: -- Управление состоянием -- GraphQL операции -- Переключение между табами - -**EmployeeCard** - карточка сотрудника: -- Отображение информации -- Кнопки действий -- Анимации hover - -**EmployeeForm** - форма создания/редактирования: -- Валидация полей -- Маски ввода -- Загрузка файлов -- Обработка ошибок - -**EmployeeSchedule** - календарь: -- Интерактивный календарь -- Управление статусами дней -- Подсчёт статистики - -### 3. Требования к UX/UI - -**Интерактивность:** -- Плавные анимации при hover -- Loading состояния для всех операций -- Оптимистичные обновления -- Toast уведомления об успехе/ошибке - -**Адаптивность:** -- Корректное отображение на мобильных устройствах -- Responsive grid для карточек -- Адаптивная форма - -**Доступность:** -- Поддержка клавиатурной навигации -- ARIA атрибуты -- Семантическая разметка - -### 4. Валидация - -**Клиентская валидация:** -- Обязательные поля -- Форматы email, телефона -- Валидные даты -- Паспортные данные - -**Серверная валидация:** -- Дублирование всех проверок -- Уникальность телефона -- Проверка существования сотрудника - -### 5. Обработка ошибок - -- Graceful обработка всех ошибок API -- Понятные сообщения пользователю -- Retry механизм для сетевых ошибок -- Fallback состояния - -## Дополнительные фичи (nice to have) - -1. **Экспорт данных** - выгрузка списка сотрудников в Excel/PDF -2. **Массовые операции** - выбор нескольких сотрудников для действий -3. **Фильтры** - по отделу, статусу, дате приёма -4. **Сортировка** - по ФИО, дате приёма, зарплате -5. **История изменений** - лог всех изменений сотрудника -6. **Интеграция с мессенджерами** - отправка уведомлений - -## Критерии оценки - -### Обязательно (must have): -- ✅ Все основные функции работают -- ✅ Соответствие дизайн-системе -- ✅ Чистый и понятный код -- ✅ TypeScript без any -- ✅ Обработка ошибок -- ✅ Валидация данных -- ✅ Адаптивная вёрстка - -### Дополнительные баллы: -- ⭐ Оптимизация производительности -- ⭐ Тесты (unit/integration) -- ⭐ Документация API -- ⭐ Инновационные решения UX -- ⭐ Дополнительные фичи - -## Ресурсы - -### Дизайн-референсы: -- Glassmorphism: https://css.glass/ -- UI Patterns: https://ui-patterns.com/ - -### Технические ресурсы: -- Next.js App Router: https://nextjs.org/docs -- Prisma: https://www.prisma.io/docs -- Apollo GraphQL: https://www.apollographql.com/docs -- shadcn/ui: https://ui.shadcn.com/ - -## Дедлайн - -**Время выполнения:** 2 часа - -**Что предоставить:** -1. GitHub репозиторий с кодом -2. README с инструкциями по запуску -3. Демо на Vercel/Netlify (по возможности) -4. Краткое описание архитектурных решений - ---- - -*Удачи в разработке! 🚀* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 389ce34..8cf86a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-imask": "^7.6.1", + "react-resizable-panels": "^3.0.3", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1" }, @@ -9634,6 +9635,16 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.3.tgz", + "integrity": "sha512-7HA8THVBHTzhDK4ON0tvlGXyMAJN1zBeRpuyyremSikgYh2ku6ltD7tsGQOcXx4NKPrZtYCm/5CBr+dkruTGQw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", diff --git a/package.json b/package.json index 01a5d5b..8dc80d4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-imask": "^7.6.1", + "react-resizable-panels": "^3.0.3", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1" }, diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 1bf0abb..b381976 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -2,6 +2,7 @@ import { ApolloProvider } from '@apollo/client' import { apolloClient } from '@/lib/apollo-client' +import { SidebarProvider } from '@/hooks/useSidebar' export function Providers({ children, @@ -10,7 +11,9 @@ export function Providers({ }) { return ( - {children} + + {children} + ) } \ No newline at end of file diff --git a/src/components/admin/categories-section.tsx b/src/components/admin/categories-section.tsx index 87c93bc..a7bf216 100644 --- a/src/components/admin/categories-section.tsx +++ b/src/components/admin/categories-section.tsx @@ -27,6 +27,22 @@ export function CategoriesSection() { const [newCategoryName, setNewCategoryName] = useState('') const [editCategoryName, setEditCategoryName] = useState('') + const formatDate = (dateString: string) => { + try { + const date = new Date(dateString) + if (isNaN(date.getTime())) { + return 'Неизвестно' + } + return date.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }) + } catch (error) { + return 'Неизвестно' + } + } + const { data, loading, error, refetch } = useQuery(GET_CATEGORIES) const [createCategory, { loading: creating }] = useMutation(CREATE_CATEGORY) const [updateCategory, { loading: updating }] = useMutation(UPDATE_CATEGORY) @@ -266,7 +282,7 @@ export function CategoriesSection() {

{category.name}

- Создано: {new Date(category.createdAt).toLocaleDateString('ru-RU')} + Создано: {formatDate(category.createdAt)}

diff --git a/src/components/admin/users-section.tsx b/src/components/admin/users-section.tsx index b061c83..9729501 100644 --- a/src/components/admin/users-section.tsx +++ b/src/components/admin/users-section.tsx @@ -96,13 +96,21 @@ export function UsersSection() { } const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString('ru-RU', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) + try { + const date = new Date(dateString) + if (isNaN(date.getTime())) { + return 'Неизвестно' + } + return date.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } catch (error) { + return 'Неизвестно' + } } const getInitials = (name?: string, phone?: string) => { diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index 43c82b4..f613a82 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -1,6 +1,7 @@ "use client" import { useAuth } from '@/hooks/useAuth' +import { useSidebar } from '@/hooks/useSidebar' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' @@ -14,13 +15,16 @@ import { Warehouse, Users, Truck, - Handshake + Handshake, + ChevronLeft, + ChevronRight } from 'lucide-react' export function Sidebar() { const { user, logout } = useAuth() const router = useRouter() const pathname = usePathname() + const { isCollapsed, toggleSidebar } = useSidebar() const getInitials = () => { const orgName = getOrganizationName() @@ -86,6 +90,8 @@ export function Sidebar() { router.push('/partners') } + + const isSettingsActive = pathname === '/settings' const isMarketActive = pathname.startsWith('/market') const isMessengerActive = pathname.startsWith('/messenger') @@ -96,96 +102,149 @@ export function Sidebar() { const isPartnersActive = pathname.startsWith('/partners') return ( -
+
- + {/* Кнопка сворачивания */} +
+ +
{/* Информация о пользователе */} -
-
- - {user?.avatar ? ( - - ) : null} - - {getInitials()} - - -
-
-
-

- {getOrganizationName()} -

-
-
-

- {getCabinetType()} + {!isCollapsed ? ( + // Развернутое состояние +

+
+ + {user?.avatar ? ( + + ) : null} + + {getInitials()} + + +
+
+
+

+ {getOrganizationName()}

+
+
+

+ {getCabinetType()} +

+
-
+ ) : ( + // Свернутое состояние +
+
+ + {user?.avatar ? ( + + ) : null} + + {getInitials()} + + +
+
+
+

+ {getOrganizationName().length > 12 + ? getOrganizationName().substring(0, 12) + '...' + : getOrganizationName() + } +

+
+
+
+
+
+ )} {/* Навигация */} -
+
{/* Услуги - только для фулфилмент центров */} {user?.organization?.type === 'FULFILLMENT' && ( )} @@ -193,15 +252,16 @@ export function Sidebar() { {user?.organization?.type === 'FULFILLMENT' && ( )} @@ -209,15 +269,16 @@ export function Sidebar() { {user?.organization?.type === 'SELLER' && ( )} @@ -225,41 +286,44 @@ export function Sidebar() { {user?.organization?.type === 'WHOLESALE' && ( )}
{/* Кнопка выхода */} -
+
diff --git a/src/components/dashboard/user-settings.tsx b/src/components/dashboard/user-settings.tsx index 7703b47..a7f7c4d 100644 --- a/src/components/dashboard/user-settings.tsx +++ b/src/components/dashboard/user-settings.tsx @@ -14,6 +14,7 @@ import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription } from '@/components/ui/alert' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Sidebar } from './sidebar' +import { useSidebar } from '@/hooks/useSidebar' import { User, Building2, @@ -38,6 +39,7 @@ import { useState, useEffect } from 'react' import Image from 'next/image' export function UserSettings() { + const { getSidebarMargin } = useSidebar() const { user } = useAuth() const [updateUserProfile, { loading: isSaving }] = useMutation(UPDATE_USER_PROFILE) const [updateOrganizationByInn, { loading: isUpdatingOrganization }] = useMutation(UPDATE_ORGANIZATION_BY_INN) @@ -552,7 +554,7 @@ export function UserSettings() { return (
-
+
{/* Сообщения о сохранении */} {saveMessage && ( diff --git a/src/components/market/market-dashboard.tsx b/src/components/market/market-dashboard.tsx index d2c894b..e7cd994 100644 --- a/src/components/market/market-dashboard.tsx +++ b/src/components/market/market-dashboard.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Card } from '@/components/ui/card' import { Sidebar } from '@/components/dashboard/sidebar' +import { useSidebar } from '@/hooks/useSidebar' import { MarketProducts } from './market-products' import { MarketCategories } from './market-categories' import { MarketRequests } from './market-requests' @@ -12,6 +13,7 @@ import { MarketBusiness } from './market-business' import { FavoritesDashboard } from '../favorites/favorites-dashboard' export function MarketDashboard() { + const { getSidebarMargin } = useSidebar() const [productsView, setProductsView] = useState<'categories' | 'products' | 'cart' | 'favorites'>('categories') const [selectedCategory, setSelectedCategory] = useState<{ id: string; name: string } | null>(null) @@ -38,7 +40,7 @@ export function MarketDashboard() { return (
-
+
{/* Основной контент с табами */}
diff --git a/src/components/messenger/messenger-dashboard.tsx b/src/components/messenger/messenger-dashboard.tsx index 47d655d..fa56874 100644 --- a/src/components/messenger/messenger-dashboard.tsx +++ b/src/components/messenger/messenger-dashboard.tsx @@ -1,22 +1,17 @@ "use client" -import React, { useState, useRef, useCallback } from 'react' +import React, { useState } from 'react' import { useQuery } from '@apollo/client' import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Sidebar } from '@/components/dashboard/sidebar' +import { useSidebar } from '@/hooks/useSidebar' import { MessengerConversations } from './messenger-conversations' import { MessengerChat } from './messenger-chat' import { MessengerEmptyState } from './messenger-empty-state' import { GET_MY_COUNTERPARTIES } from '@/graphql/queries' -import { - MessageCircle, - PanelLeftOpen, - PanelLeftClose, - Maximize2, - Minimize2, - Settings -} from 'lucide-react' +import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels' +import { MessageCircle } from 'lucide-react' interface Organization { id: string @@ -31,14 +26,9 @@ interface Organization { createdAt: string } -type LeftPanelSize = 'compact' | 'normal' | 'wide' | 'hidden' - export function MessengerDashboard() { + const { getSidebarMargin } = useSidebar() const [selectedCounterparty, setSelectedCounterparty] = useState(null) - const [leftPanelSize, setLeftPanelSize] = useState('normal') - const [isResizing, setIsResizing] = useState(false) - const [leftPanelWidth, setLeftPanelWidth] = useState(350) - const resizeRef = useRef(null) const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES) const counterparties = counterpartiesData?.myCounterparties || [] @@ -49,85 +39,13 @@ export function MessengerDashboard() { const selectedCounterpartyData = counterparties.find((cp: Organization) => cp.id === selectedCounterparty) - // Получение ширины для разных размеров панели - const getPanelWidth = (size: LeftPanelSize) => { - switch (size) { - case 'hidden': return 0 - case 'compact': return 280 - case 'normal': return 350 - case 'wide': return 450 - default: return 350 - } - } - - const currentWidth = leftPanelSize === 'normal' ? leftPanelWidth : getPanelWidth(leftPanelSize) - - // Обработка изменения размера панели - const handleMouseDown = useCallback((e: React.MouseEvent) => { - setIsResizing(true) - e.preventDefault() - }, []) - - const handleMouseMove = useCallback((e: MouseEvent) => { - if (!isResizing) return - - const newWidth = Math.min(Math.max(280, e.clientX - 56 - 24), 600) // 56px sidebar + 24px padding - setLeftPanelWidth(newWidth) - setLeftPanelSize('normal') - }, [isResizing]) - - const handleMouseUp = useCallback(() => { - setIsResizing(false) - }, []) - - // Добавляем глобальные обработчики для изменения размера - React.useEffect(() => { - if (isResizing) { - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - document.body.style.cursor = 'col-resize' - document.body.style.userSelect = 'none' - } else { - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - document.body.style.cursor = '' - document.body.style.userSelect = '' - } - - return () => { - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - document.body.style.cursor = '' - document.body.style.userSelect = '' - } - }, [isResizing, handleMouseMove, handleMouseUp]) - - // Переключение размеров панели - const togglePanelSize = () => { - const sizes: LeftPanelSize[] = ['compact', 'normal', 'wide'] - const currentIndex = sizes.indexOf(leftPanelSize) - const nextIndex = (currentIndex + 1) % sizes.length - setLeftPanelSize(sizes[nextIndex]) - } - - const togglePanelVisibility = () => { - setLeftPanelSize(leftPanelSize === 'hidden' ? 'normal' : 'hidden') - } - // Если нет контрагентов, показываем заглушку if (!counterpartiesLoading && counterparties.length === 0) { return (
-
+
-
-
-

Мессенджер

-

Общение с контрагентами

-
-
-
@@ -142,115 +60,56 @@ export function MessengerDashboard() { return (
-
+
- {/* Заголовок с управлением панелями */} -
-
-

Мессенджер

-

Общение с контрагентами

-
- - {/* Управление панелями */} -
- - - {leftPanelSize !== 'hidden' && ( - - )} -
-
- {/* Основной контент */}
-
+ {/* Левая панель - список контрагентов */} - {leftPanelSize !== 'hidden' && ( - <> - - - + + + + + - {/* Разделитель для изменения размера */} - {leftPanelSize === 'normal' && ( -
-
-
-
- )} - - )} + {/* Разделитель для изменения размера */} + +
+
+ {/* Правая панель - чат */} - - {selectedCounterparty && selectedCounterpartyData ? ( - - ) : ( -
-
-
- + + + {selectedCounterparty && selectedCounterpartyData ? ( + + ) : ( +
+
+
+ +
+

Выберите контрагента

+

+ Начните беседу с одним из ваших контрагентов +

-

Выберите контрагента

-

- Начните беседу с одним из ваших контрагентов -

- {leftPanelSize === 'hidden' && ( - - )}
-
- )} - -
+ )} + + +
diff --git a/src/components/partners/partners-dashboard.tsx b/src/components/partners/partners-dashboard.tsx index 59cd4eb..5321459 100644 --- a/src/components/partners/partners-dashboard.tsx +++ b/src/components/partners/partners-dashboard.tsx @@ -3,6 +3,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Card } from '@/components/ui/card' import { Sidebar } from '@/components/dashboard/sidebar' +import { useSidebar } from '@/hooks/useSidebar' import { MarketCounterparties } from '../market/market-counterparties' import { MarketFulfillment } from '../market/market-fulfillment' import { MarketSellers } from '../market/market-sellers' @@ -10,10 +11,11 @@ import { MarketLogistics } from '../market/market-logistics' import { MarketWholesale } from '../market/market-wholesale' export function PartnersDashboard() { + const { getSidebarMargin } = useSidebar() return (
-
+
{/* Основной контент с табами */}
diff --git a/src/components/services/services-dashboard.tsx b/src/components/services/services-dashboard.tsx index 93c0dd8..cca22f7 100644 --- a/src/components/services/services-dashboard.tsx +++ b/src/components/services/services-dashboard.tsx @@ -2,15 +2,17 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Sidebar } from '@/components/dashboard/sidebar' +import { useSidebar } from '@/hooks/useSidebar' import { ServicesTab } from './services-tab' import { SuppliesTab } from './supplies-tab' import { LogisticsTab } from './logistics-tab' export function ServicesDashboard() { + const { getSidebarMargin } = useSidebar() return (
-
+
{/* Основной контент с табами */}
diff --git a/src/components/warehouse/warehouse-dashboard.tsx b/src/components/warehouse/warehouse-dashboard.tsx index bf4e311..bd2c85d 100644 --- a/src/components/warehouse/warehouse-dashboard.tsx +++ b/src/components/warehouse/warehouse-dashboard.tsx @@ -6,6 +6,7 @@ import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Sidebar } from '@/components/dashboard/sidebar' +import { useSidebar } from '@/hooks/useSidebar' import { ProductForm } from './product-form' import { ProductCard } from './product-card' import { GET_MY_PRODUCTS } from '@/graphql/queries' @@ -34,6 +35,7 @@ interface Product { } export function WarehouseDashboard() { + const { getSidebarMargin } = useSidebar() const [isDialogOpen, setIsDialogOpen] = useState(false) const [editingProduct, setEditingProduct] = useState(null) const [searchQuery, setSearchQuery] = useState('') @@ -104,7 +106,7 @@ export function WarehouseDashboard() { return (
-
+
{/* Заголовок и поиск */}
@@ -114,14 +116,6 @@ export function WarehouseDashboard() {
- -