diff --git a/src/app/employees/page.tsx b/src/app/employees/page.tsx new file mode 100644 index 0000000..3d0154d --- /dev/null +++ b/src/app/employees/page.tsx @@ -0,0 +1,10 @@ +import { AuthGuard } from "@/components/auth-guard" +import { EmployeesDashboard } from "@/components/employees/employees-dashboard" + +export default function EmployeesPage() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index 457f4d8..0f4c9ae 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -11,7 +11,8 @@ import { Store, MessageCircle, Wrench, - Warehouse + Warehouse, + Users } from 'lucide-react' export function Sidebar() { @@ -71,11 +72,16 @@ export function Sidebar() { router.push('/warehouse') } + const handleEmployeesClick = () => { + router.push('/employees') + } + const isSettingsActive = pathname === '/settings' const isMarketActive = pathname.startsWith('/market') const isMessengerActive = pathname.startsWith('/messenger') const isServicesActive = pathname.startsWith('/services') const isWarehouseActive = pathname.startsWith('/warehouse') + const isEmployeesActive = pathname.startsWith('/employees') return (
@@ -158,6 +164,22 @@ export function Sidebar() { )} + {/* Сотрудники - только для фулфилмент центров */} + {user?.organization?.type === 'FULFILLMENT' && ( + + )} + {/* Склад - только для оптовиков */} {user?.organization?.type === 'WHOLESALE' && ( + +

+ {monthNames[currentMonth]} {currentYear} +

+ + +
+ +
+ + + +
+ + + {/* Легенда */} +
+ {[ + { status: 'work' as WorkDayStatus, label: 'Рабочий день' }, + { status: 'weekend' as WorkDayStatus, label: 'Выходной' }, + { status: 'vacation' as WorkDayStatus, label: 'Отпуск' }, + { status: 'sick' as WorkDayStatus, label: 'Больничный' }, + { status: 'absent' as WorkDayStatus, label: 'Прогул' } + ].map(({ status, label }) => ( +
+
+ {getStatusIcon(status)} +
+ {label} +
+ ))} +
+ + + {/* Календарь для каждого сотрудника */} + {(selectedEmployee === 'all' ? scheduleEmployees : scheduleEmployees.filter(e => e.id === selectedEmployee)).map(employee => { + const stats = getMonthStats(employee.id) + + return ( + + {/* Информация о сотруднике */} +
+
+ + {employee.avatar ? ( + + ) : null} + + {employee.name.split(' ').map(n => n.charAt(0)).join('')} + + +
+

{employee.name}

+

Табель работы за {monthNames[currentMonth].toLowerCase()}

+
+
+ + {/* Статистика */} +
+
+

{stats.workDays}

+

Рабочих

+
+
+

{stats.vacationDays}

+

Отпуск

+
+
+

{stats.sickDays}

+

Больничный

+
+
+

{stats.absentDays}

+

Прогулы

+
+
+

{stats.totalHours}ч

+

Всего часов

+
+
+
+ + {/* Сетка календаря */} +
+ {/* Заголовки дней недели */} + {weekDays.map(day => ( +
+ {day} +
+ ))} + + {/* Дни месяца */} + {calendarDays.map((day, index) => { + if (day === null) { + return
+ } + + const status = getDayStatus(employee.id, day) + const hours = getDayHours(employee.id, day) + const isToday = new Date().getDate() === day && + new Date().getMonth() === currentMonth && + new Date().getFullYear() === currentYear + + return ( +
{ + // Циклично переключаем статусы + const statuses: WorkDayStatus[] = ['work', 'weekend', 'vacation', 'sick', 'absent'] + const currentIndex = statuses.indexOf(status) + const nextStatus = statuses[(currentIndex + 1) % statuses.length] + changeDayStatus(employee.id, day, nextStatus) + }} + > +
+
+ {getStatusIcon(status)} + {day} +
+ {hours > 0 && ( + {hours}ч + )} +
+ + {isToday && ( +
+ )} +
+ ) + })} +
+
+ ) + })} + + ) +} \ No newline at end of file diff --git a/src/components/employees/employees-dashboard.tsx b/src/components/employees/employees-dashboard.tsx new file mode 100644 index 0000000..fcc0f5d --- /dev/null +++ b/src/components/employees/employees-dashboard.tsx @@ -0,0 +1,106 @@ +"use client" + +import { useState } from 'react' +import { Sidebar } from '@/components/dashboard/sidebar' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Input } from '@/components/ui/input' +import { EmployeesList } from './employees-list' +import { EmployeeSchedule } from './employee-schedule' +import { + Users, + Calendar, + Search, + Plus, + FileText +} from 'lucide-react' + +export function EmployeesDashboard() { + const [searchQuery, setSearchQuery] = useState('') + + return ( +
+ +
+
+ {/* Заголовок страницы */} +
+
+ +
+

Управление сотрудниками

+

Личные данные, табель работы и учет

+
+
+ +
+ + {/* Поиск */} + +
+ + setSearchQuery(e.target.value)} + className="glass-input pl-10" + /> +
+
+ + {/* Основной контент с вкладками */} + + + + + Список сотрудников + + + + Табель работы + + + + Отчеты + + + + + + + + + + + + + +
+ +

Отчеты

+

+ Генерация отчетов по работе сотрудников +

+

Функция в разработке

+
+
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employees-list.tsx b/src/components/employees/employees-list.tsx new file mode 100644 index 0000000..e835e80 --- /dev/null +++ b/src/components/employees/employees-list.tsx @@ -0,0 +1,413 @@ +"use client" + +import { useState } from 'react' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' +import { Badge } from '@/components/ui/badge' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { + Edit, + Phone, + Mail, + Calendar, + MapPin, + User, + Briefcase, + Save, + X +} from 'lucide-react' + +// Интерфейс сотрудника +interface Employee { + id: string + firstName: string + lastName: string + position: string + department: string + phone: string + email: string + avatar?: string + hireDate: string + status: 'active' | 'vacation' | 'sick' | 'inactive' + salary: number + address: string +} + +// Моковые данные сотрудников +const mockEmployees: Employee[] = [ + { + id: '1', + firstName: 'Александр', + lastName: 'Петров', + position: 'Менеджер склада', + department: 'Логистика', + phone: '+7 (999) 123-45-67', + email: 'a.petrov@company.com', + hireDate: '2023-01-15', + status: 'active', + salary: 80000, + address: 'Москва, ул. Ленина, 10' + }, + { + id: '2', + firstName: 'Мария', + lastName: 'Иванова', + position: 'Кладовщик', + department: 'Логистика', + phone: '+7 (999) 234-56-78', + email: 'm.ivanova@company.com', + hireDate: '2023-03-20', + status: 'active', + salary: 60000, + address: 'Москва, ул. Советская, 25' + }, + { + id: '3', + firstName: 'Дмитрий', + lastName: 'Сидоров', + position: 'Водитель', + department: 'Доставка', + phone: '+7 (999) 345-67-89', + email: 'd.sidorov@company.com', + hireDate: '2022-11-10', + status: 'vacation', + salary: 70000, + address: 'Москва, ул. Мира, 15' + }, + { + id: '4', + firstName: 'Анна', + lastName: 'Козлова', + position: 'HR-специалист', + department: 'Кадры', + phone: '+7 (999) 456-78-90', + email: 'a.kozlova@company.com', + hireDate: '2023-02-05', + status: 'active', + salary: 75000, + address: 'Москва, пр. Победы, 8' + } +] + +interface EmployeesListProps { + searchQuery: string +} + +export function EmployeesList({ searchQuery }: EmployeesListProps) { + const [employees, setEmployees] = useState(mockEmployees) + const [selectedEmployee, setSelectedEmployee] = useState(null) + const [isEditModalOpen, setIsEditModalOpen] = useState(false) + + // Фильтрация сотрудников по поисковому запросу + const filteredEmployees = employees.filter(employee => + `${employee.firstName} ${employee.lastName}`.toLowerCase().includes(searchQuery.toLowerCase()) || + employee.position.toLowerCase().includes(searchQuery.toLowerCase()) || + employee.department.toLowerCase().includes(searchQuery.toLowerCase()) + ) + + const getStatusBadge = (status: Employee['status']) => { + switch (status) { + case 'active': + return Активен + case 'vacation': + return В отпуске + case 'sick': + return На больничном + case 'inactive': + return Неактивен + default: + return Неизвестно + } + } + + const getInitials = (firstName: string, lastName: string) => { + return `${firstName.charAt(0)}${lastName.charAt(0)}` + } + + const formatSalary = (salary: number) => { + return new Intl.NumberFormat('ru-RU').format(salary) + ' ₽' + } + + const handleEditEmployee = (employee: Employee) => { + setSelectedEmployee(employee) + setIsEditModalOpen(true) + } + + const handleSaveEmployee = () => { + if (selectedEmployee) { + setEmployees(prev => + prev.map(emp => emp.id === selectedEmployee.id ? selectedEmployee : emp) + ) + setIsEditModalOpen(false) + setSelectedEmployee(null) + } + } + + return ( +
+ {/* Статистика */} +
+ +
+
+

Всего сотрудников

+

{employees.length}

+
+ +
+
+ +
+
+

Активных

+

+ {employees.filter(e => e.status === 'active').length} +

+
+ +
+
+ +
+
+

В отпуске

+

+ {employees.filter(e => e.status === 'vacation').length} +

+
+ +
+
+ +
+
+

Отделов

+

+ {new Set(employees.map(e => e.department)).size} +

+
+ +
+
+
+ + {/* Список сотрудников */} +
+ {filteredEmployees.map((employee) => ( + +
+ + {employee.avatar ? ( + + ) : null} + + {getInitials(employee.firstName, employee.lastName)} + + + +
+
+

+ {employee.firstName} {employee.lastName} +

+ +
+ +

{employee.position}

+

{employee.department}

+ +
+ {getStatusBadge(employee.status)} + + {formatSalary(employee.salary)} + +
+ +
+
+ + {employee.phone} +
+
+ + {employee.email} +
+
+ + С {new Date(employee.hireDate).toLocaleDateString('ru-RU')} +
+
+
+
+
+ ))} +
+ + {/* Модальное окно редактирования */} + + + + + + Редактировать сотрудника + + + + {selectedEmployee && ( +
+
+
+ + setSelectedEmployee({ + ...selectedEmployee, + firstName: e.target.value + })} + className="glass-input text-white" + /> +
+
+ + setSelectedEmployee({ + ...selectedEmployee, + lastName: e.target.value + })} + className="glass-input text-white" + /> +
+
+ +
+ + setSelectedEmployee({ + ...selectedEmployee, + position: e.target.value + })} + className="glass-input text-white" + /> +
+ +
+ + setSelectedEmployee({ + ...selectedEmployee, + department: e.target.value + })} + className="glass-input text-white" + /> +
+ +
+ + +
+ +
+
+ + setSelectedEmployee({ + ...selectedEmployee, + phone: e.target.value + })} + className="glass-input text-white" + /> +
+
+ + setSelectedEmployee({ + ...selectedEmployee, + salary: Number(e.target.value) + })} + className="glass-input text-white" + /> +
+
+ +
+ + setSelectedEmployee({ + ...selectedEmployee, + email: e.target.value + })} + className="glass-input text-white" + /> +
+ +
+ + setSelectedEmployee({ + ...selectedEmployee, + address: e.target.value + })} + className="glass-input text-white" + /> +
+ +
+ + +
+
+ )} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/favorites/favorites-items.tsx b/src/components/favorites/favorites-items.tsx index 22857d6..aef3895 100644 --- a/src/components/favorites/favorites-items.tsx +++ b/src/components/favorites/favorites-items.tsx @@ -217,7 +217,7 @@ export function FavoritesItems({ favorites, onBackToCategories }: FavoritesItems {/* Товары этого поставщика */} -
+
{group.products.map((product) => { const isLoading = loadingItems.has(product.id) const mainImage = product.images?.[0] || product.mainImage @@ -280,9 +280,6 @@ export function FavoritesItems({ favorites, onBackToCategories }: FavoritesItems {/* Статус наличия */}
- - {product.quantity} шт. - {product.quantity > 0 ? ( В наличии @@ -292,6 +289,9 @@ export function FavoritesItems({ favorites, onBackToCategories }: FavoritesItems Нет в наличии )} + + {product.quantity} шт. +