From 84720a634d3436b06c3cf39901eb4b76189fad12 Mon Sep 17 00:00:00 2001 From: Bivekich Date: Wed, 30 Jul 2025 15:40:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D0=B8=20=D1=83=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BE=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=B4=D0=BD=D0=B8=D0=BA=D0=B0=D0=BC=D0=B8:=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B0,=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0,=20=D0=BB=D0=B5=D0=B3=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4=D1=8B=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=83=D1=81=D1=82=D0=BE=D0=B3=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BA=D0=B0.=20=D0=9E=D0=BF=D1=82=D0=B8?= =?UTF-8?q?=D0=BC=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BE=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D0=B8=D1=85=20=D1=82=D0=B0=D0=B1=D0=B5=D0=BB=D0=B5=D0=B9.=20?= =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B5=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=87=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../employees/employee-calendar.tsx | 161 +++++ src/components/employees/employee-card.tsx | 200 +++++ .../employees/employee-empty-state.tsx | 39 + src/components/employees/employee-header.tsx | 30 + src/components/employees/employee-item.tsx | 96 +++ src/components/employees/employee-legend.tsx | 40 + src/components/employees/employee-reports.tsx | 191 +++++ src/components/employees/employee-search.tsx | 26 + src/components/employees/employee-stats.tsx | 76 ++ .../employees/employees-dashboard.tsx | 683 ++---------------- src/components/employees/month-navigation.tsx | 32 + 11 files changed, 937 insertions(+), 637 deletions(-) create mode 100644 src/components/employees/employee-calendar.tsx create mode 100644 src/components/employees/employee-card.tsx create mode 100644 src/components/employees/employee-empty-state.tsx create mode 100644 src/components/employees/employee-header.tsx create mode 100644 src/components/employees/employee-item.tsx create mode 100644 src/components/employees/employee-legend.tsx create mode 100644 src/components/employees/employee-reports.tsx create mode 100644 src/components/employees/employee-search.tsx create mode 100644 src/components/employees/employee-stats.tsx create mode 100644 src/components/employees/month-navigation.tsx diff --git a/src/components/employees/employee-calendar.tsx b/src/components/employees/employee-calendar.tsx new file mode 100644 index 0000000..f9e3b71 --- /dev/null +++ b/src/components/employees/employee-calendar.tsx @@ -0,0 +1,161 @@ +"use client" + +import { Calendar, CheckCircle, Clock, Plane, Activity, XCircle } from 'lucide-react' + +interface ScheduleRecord { + id: string + date: string + status: string + hoursWorked?: number + employee: { + id: string + } +} + +interface EmployeeCalendarProps { + employeeId: string + employeeSchedules: {[key: string]: ScheduleRecord[]} + currentYear: number + currentMonth: number + onDayStatusChange: (employeeId: string, day: number, currentStatus: string) => void +} + +export function EmployeeCalendar({ + employeeId, + employeeSchedules, + currentYear, + currentMonth, + onDayStatusChange +}: EmployeeCalendarProps) { + // Получаем количество дней в месяце + const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate() + + // Функция для получения статуса дня + const getDayStatus = (day: number) => { + const date = new Date(currentYear, currentMonth, day) + const dateStr = date.toISOString().split('T')[0] // YYYY-MM-DD + + // Ищем запись в табеле для этого дня + const scheduleData = employeeSchedules[employeeId] || [] + const dayRecord = scheduleData.find(record => + record.date.split('T')[0] === dateStr + ) + + if (dayRecord) { + return dayRecord.status.toLowerCase() + } + + // Если записи нет, устанавливаем дефолтный статус + const dayOfWeek = date.getDay() + if (dayOfWeek === 0 || dayOfWeek === 6) return 'weekend' + return 'work' // По умолчанию рабочий день для новых сотрудников + } + + const getCellStyle = (status: string) => { + switch (status) { + case 'work': + return 'bg-emerald-500/20 text-emerald-300/70 border-emerald-400/80' + case 'weekend': + return 'bg-purple-500/20 text-purple-300/70 border-purple-400/80' + case 'vacation': + return 'bg-blue-500/20 text-blue-300/70 border-blue-400/80' + case 'sick': + return 'bg-yellow-500/20 text-yellow-300/70 border-yellow-400/80' + case 'absent': + return 'bg-red-500/20 text-red-300/70 border-red-400/80' + default: + return 'bg-white/10 text-white/50 border-white/20' + } + } + + const getStatusIcon = (status: string) => { + switch (status) { + case 'work': + return + case 'weekend': + return + case 'vacation': + return + case 'sick': + return + case 'absent': + return + default: + return null + } + } + + // Создаем массив дней календаря + const calendarDays: (number | null)[] = [] + const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay() + const startOffset = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1 + + // Добавляем пустые ячейки для выравнивания первой недели + for (let i = 0; i < startOffset; i++) { + calendarDays.push(null) + } + + // Добавляем дни месяца + for (let day = 1; day <= daysInMonth; day++) { + calendarDays.push(day) + } + + return ( +
+

+ + Табель работы за {new Date(currentYear, currentMonth).toLocaleDateString('ru-RU', { month: 'long' })} +

+ + {/* Сетка календаря */} +
+ {/* Заголовки дней недели */} + {['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => ( +
+ {day} +
+ ))} + + {/* Дни месяца */} + {calendarDays.map((day, index) => { + if (day === null) { + return
+ } + + const status = getDayStatus(day) + const hours = status === 'work' ? 8 : status === 'vacation' || status === 'sick' ? 8 : 0 + const isToday = new Date().getDate() === day && + new Date().getMonth() === currentMonth && + new Date().getFullYear() === currentYear + + return ( +
onDayStatusChange(employeeId, day, status)} + > +
+
+ {getStatusIcon(status)} + {day} +
+ {hours > 0 && ( + {hours}ч + )} +
+ + {isToday && ( +
+ )} +
+ ) + })} +
+
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employee-card.tsx b/src/components/employees/employee-card.tsx new file mode 100644 index 0000000..3e7cb68 --- /dev/null +++ b/src/components/employees/employee-card.tsx @@ -0,0 +1,200 @@ +"use client" + +import { Button } from '@/components/ui/button' +import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog' +import { + Edit, + UserX, + Phone, + Mail, + Calendar, + Briefcase, + MapPin, + AlertCircle, + MessageCircle +} from 'lucide-react' + +interface Employee { + id: string + firstName: string + lastName: string + middleName?: string + position: string + phone: string + email?: string + avatar?: string + hireDate: string + status: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED' + salary?: number + address?: string + birthDate?: string + passportSeries?: string + passportNumber?: string + passportIssued?: string + passportDate?: string + emergencyContact?: string + emergencyPhone?: string + telegram?: string + whatsapp?: string + passportPhoto?: string + createdAt: string + updatedAt: string +} + +interface EmployeeCardProps { + employee: Employee + onEdit: (employee: Employee) => void + onDelete: (employeeId: string) => void + deletingEmployeeId: string | null +} + +export function EmployeeCard({ employee, onEdit, onDelete, deletingEmployeeId }: EmployeeCardProps) { + return ( +
+
+ + {employee.avatar ? ( + { + console.error('Ошибка загрузки аватара:', employee.avatar); + e.currentTarget.style.display = 'none'; + }} + onLoad={() => console.log('Аватар загружен успешно:', employee.avatar)} + /> + ) : null} + + {employee.firstName.charAt(0)}{employee.lastName.charAt(0)} + + + +
+
+

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

+
+ + + + + + + + + Уволить сотрудника? + + Вы уверены, что хотите уволить сотрудника {employee.firstName} {employee.lastName}? + Это действие нельзя отменить. + + + + + Отмена + + onDelete(employee.id)} + disabled={deletingEmployeeId === employee.id} + className="bg-red-600 hover:bg-red-700 text-white" + > + {deletingEmployeeId === employee.id ? 'Увольнение...' : 'Уволить'} + + + + +
+
+ +
+

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

+

{employee.position}

+
+ +
+ {/* Основные контакты */} +
+ + {employee.phone} +
+ {employee.email && ( +
+ + {employee.email} +
+ )} + + {/* Дата рождения */} + {employee.birthDate && ( +
+ + + Родился: {new Date(employee.birthDate).toLocaleDateString('ru-RU')} + +
+ )} + + {/* Дата приема на работу */} +
+ + + Принят: {new Date(employee.hireDate).toLocaleDateString('ru-RU')} + +
+ + {/* Адрес */} + {employee.address && ( +
+ + {employee.address} +
+ )} + + {/* Экстренный контакт */} + {employee.emergencyContact && ( +
+ + + Экстр. контакт: {employee.emergencyContact} + {employee.emergencyPhone && ` (${employee.emergencyPhone})`} + +
+ )} + + {/* Мессенджеры */} +
+ {employee.telegram && ( +
+ + @{employee.telegram} +
+ )} + {employee.whatsapp && ( +
+ + {employee.whatsapp} +
+ )} +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employee-empty-state.tsx b/src/components/employees/employee-empty-state.tsx new file mode 100644 index 0000000..cb7cac2 --- /dev/null +++ b/src/components/employees/employee-empty-state.tsx @@ -0,0 +1,39 @@ +"use client" + +import { Button } from '@/components/ui/button' +import { Users, Plus } from 'lucide-react' + +interface EmployeeEmptyStateProps { + searchQuery: string + onShowAddForm: () => void +} + +export function EmployeeEmptyState({ searchQuery, onShowAddForm }: EmployeeEmptyStateProps) { + return ( +
+
+
+ +
+

+ {searchQuery ? 'Сотрудники не найдены' : 'У вас пока нет сотрудников'} +

+

+ {searchQuery + ? 'Попробуйте изменить критерии поиска' + : 'Добавьте первого сотрудника в вашу команду' + } +

+ {!searchQuery && ( + + )} +
+
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employee-header.tsx b/src/components/employees/employee-header.tsx new file mode 100644 index 0000000..80c957d --- /dev/null +++ b/src/components/employees/employee-header.tsx @@ -0,0 +1,30 @@ +"use client" + +import { Button } from '@/components/ui/button' +import { Users, Plus } from 'lucide-react' + +interface EmployeeHeaderProps { + showAddForm: boolean + onToggleAddForm: () => void +} + +export function EmployeeHeader({ showAddForm, onToggleAddForm }: EmployeeHeaderProps) { + return ( +
+
+ +
+

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

+

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

+
+
+ +
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employee-item.tsx b/src/components/employees/employee-item.tsx new file mode 100644 index 0000000..43afa17 --- /dev/null +++ b/src/components/employees/employee-item.tsx @@ -0,0 +1,96 @@ +"use client" + +import { Card } from '@/components/ui/card' +import { EmployeeCard } from './employee-card' +import { EmployeeCalendar } from './employee-calendar' +import { EmployeeStats } from './employee-stats' + +interface Employee { + id: string + firstName: string + lastName: string + middleName?: string + position: string + phone: string + email?: string + avatar?: string + hireDate: string + status: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED' + salary?: number + address?: string + birthDate?: string + passportSeries?: string + passportNumber?: string + passportIssued?: string + passportDate?: string + emergencyContact?: string + emergencyPhone?: string + telegram?: string + whatsapp?: string + passportPhoto?: string + createdAt: string + updatedAt: string +} + +interface ScheduleRecord { + id: string + date: string + status: string + hoursWorked?: number + employee: { + id: string + } +} + +interface EmployeeItemProps { + employee: Employee + employeeSchedules: {[key: string]: ScheduleRecord[]} + currentYear: number + currentMonth: number + onEdit: (employee: Employee) => void + onDelete: (employeeId: string) => void + onDayStatusChange: (employeeId: string, day: number, currentStatus: string) => void + deletingEmployeeId: string | null +} + +export function EmployeeItem({ + employee, + employeeSchedules, + currentYear, + currentMonth, + onEdit, + onDelete, + onDayStatusChange, + deletingEmployeeId +}: EmployeeItemProps) { + return ( + +
+ {/* Информация о сотруднике */} + + + {/* Табель работы и статистика */} +
+ + + {/* Статистика за месяц */} + +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employee-legend.tsx b/src/components/employees/employee-legend.tsx new file mode 100644 index 0000000..c4ea2b6 --- /dev/null +++ b/src/components/employees/employee-legend.tsx @@ -0,0 +1,40 @@ +"use client" + +import { CheckCircle, Clock, Plane, Activity, XCircle } from 'lucide-react' + +export function EmployeeLegend() { + return ( +
+
+
+ +
+ Рабочий день +
+
+
+ +
+ Выходной +
+
+
+ +
+ Отпуск +
+
+
+ +
+ Больничный +
+
+
+ +
+ Прогул +
+
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employee-reports.tsx b/src/components/employees/employee-reports.tsx new file mode 100644 index 0000000..2bd3c3b --- /dev/null +++ b/src/components/employees/employee-reports.tsx @@ -0,0 +1,191 @@ +"use client" + +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { + Users, + BarChart3, + FileText, + Calendar, + Download, + Plus +} from 'lucide-react' + +interface Employee { + id: string + firstName: string + lastName: string + middleName?: string + position: string + phone: string + email?: string + avatar?: string + hireDate: string + status: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED' + salary?: number + address?: string + birthDate?: string + passportSeries?: string + passportNumber?: string + passportIssued?: string + passportDate?: string + emergencyContact?: string + emergencyPhone?: string + telegram?: string + whatsapp?: string + passportPhoto?: string + createdAt: string + updatedAt: string +} + +interface EmployeeReportsProps { + employees: Employee[] + onShowAddForm: () => void + onExportCSV: () => void + onGenerateReport: () => void +} + +export function EmployeeReports({ employees, onShowAddForm, onExportCSV, onGenerateReport }: EmployeeReportsProps) { + if (employees.length === 0) { + return ( + +
+
+
+ +
+

Нет данных для отчетов

+

+ Добавьте сотрудников, чтобы генерировать отчеты и аналитику +

+ +
+
+
+ ) + } + + return ( +
+ {/* Статистические карточки */} +
+ +
+
+

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

+

{employees.length}

+
+ +
+
+ +
+
+

Активных

+

+ {employees.filter((e: Employee) => e.status === 'ACTIVE').length} +

+
+ +
+
+ +
+
+

Средняя зарплата

+

+ {employees.length > 0 + ? Math.round(employees.reduce((sum: number, e: Employee) => sum + (e.salary || 0), 0) / employees.length).toLocaleString('ru-RU') + : '0'} ₽ +

+
+ +
+
+ +
+
+

Отделов

+

+ {new Set(employees.map((e: Employee) => e.position).filter(Boolean)).size} +

+
+ +
+
+
+ + {/* Экспорт отчетов */} + +

+ + Экспорт отчетов +

+ +
+
+

Детальный отчет (CSV)

+

+ Полная информация о всех сотрудниках в формате таблицы для Excel/Google Sheets +

+ +
+ +
+

Сводный отчет (TXT)

+

+ Краткая статистика и список сотрудников в текстовом формате +

+ +
+
+
+ + {/* Аналитика по отделам */} + +

Распределение по отделам

+
+ {Array.from(new Set(employees.map((e: Employee) => e.position).filter(Boolean)) as Set).map((position: string) => { + const positionEmployees = employees.filter((e: Employee) => e.position === position) + const percentage = Math.round((positionEmployees.length / employees.length) * 100) + + return ( +
+
+
+ {position} + {positionEmployees.length} чел. ({percentage}%) +
+
+
+
+
+
+ ) + })} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employee-search.tsx b/src/components/employees/employee-search.tsx new file mode 100644 index 0000000..8729964 --- /dev/null +++ b/src/components/employees/employee-search.tsx @@ -0,0 +1,26 @@ +"use client" + +import { Card } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Search } from 'lucide-react' + +interface EmployeeSearchProps { + searchQuery: string + onSearchChange: (query: string) => void +} + +export function EmployeeSearch({ searchQuery, onSearchChange }: EmployeeSearchProps) { + return ( + +
+ + onSearchChange(e.target.value)} + className="glass-input pl-10" + /> +
+
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employee-stats.tsx b/src/components/employees/employee-stats.tsx new file mode 100644 index 0000000..d5070e4 --- /dev/null +++ b/src/components/employees/employee-stats.tsx @@ -0,0 +1,76 @@ +"use client" + +interface EmployeeStatsProps { + currentYear: number + currentMonth: number +} + +export function EmployeeStats({ currentYear, currentMonth }: EmployeeStatsProps) { + // Генерируем дни месяца для подсчета статистики + const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate() + + // Создаем мок-статистику для демонстрации + const generateDayStatus = (day: number) => { + const date = new Date(currentYear, currentMonth, day) + const dayOfWeek = date.getDay() + + // Выходные + if (dayOfWeek === 0 || dayOfWeek === 6) return 'weekend' + + // Некоторые случайные отпуска/больничные для демонстрации + if ([15, 16].includes(day)) return 'vacation' + if ([10].includes(day)) return 'sick' + if ([22].includes(day)) return 'absent' + + return 'work' + } + + // Подсчитываем статистику + const stats = { + workDays: 0, + vacationDays: 0, + sickDays: 0, + absentDays: 0, + totalHours: 0 + } + + for (let day = 1; day <= daysInMonth; day++) { + const status = generateDayStatus(day) + switch (status) { + case 'work': + stats.workDays++ + stats.totalHours += 8 + break + case 'vacation': + stats.vacationDays++ + break + case 'sick': + stats.sickDays++ + break + case 'absent': + stats.absentDays++ + break + } + } + + return ( +
+
+

{stats.workDays}

+

Рабочих дней

+
+
+

{stats.vacationDays}

+

Отпуск

+
+
+

{stats.sickDays}

+

Больничный

+
+
+

{stats.totalHours}ч

+

Всего часов

+
+
+ ) +} \ No newline at end of file diff --git a/src/components/employees/employees-dashboard.tsx b/src/components/employees/employees-dashboard.tsx index 162c1f2..0938732 100644 --- a/src/components/employees/employees-dashboard.tsx +++ b/src/components/employees/employees-dashboard.tsx @@ -5,39 +5,20 @@ import { useQuery, useMutation } from '@apollo/client' import { apolloClient } from '@/lib/apollo-client' 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 { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' -// import { EmployeeForm } from './employee-form' import { EmployeeInlineForm } from './employee-inline-form' import { EmployeeEditInlineForm } from './employee-edit-inline-form' +import { EmployeeHeader } from './employee-header' +import { EmployeeSearch } from './employee-search' +import { EmployeeLegend } from './employee-legend' +import { MonthNavigation } from './month-navigation' +import { EmployeeEmptyState } from './employee-empty-state' +import { EmployeeItem } from './employee-item' +import { EmployeeReports } from './employee-reports' import { toast } from 'sonner' -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog' import { GET_MY_EMPLOYEES, GET_EMPLOYEE_SCHEDULE } from '@/graphql/queries' import { CREATE_EMPLOYEE, UPDATE_EMPLOYEE, DELETE_EMPLOYEE, UPDATE_EMPLOYEE_SCHEDULE } from '@/graphql/mutations' -import { - Users, - Calendar, - Search, - Plus, - FileText, - Edit, - UserX, - Phone, - Mail, - Download, - BarChart3, - CheckCircle, - XCircle, - Plane, - Activity, - Clock, - Briefcase, - MapPin, - AlertCircle, - MessageCircle -} from 'lucide-react' +import { Users, FileText } from 'lucide-react' // Интерфейс сотрудника interface Employee { @@ -360,37 +341,16 @@ ${employees.map((emp: Employee) =>
{/* Заголовок страницы */} -
-
- -
-

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

-

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

-
-
- - - -
+ setShowAddForm(!showAddForm)} + /> {/* Поиск */} - -
- - setSearchQuery(e.target.value)} - className="glass-input pl-10" - /> -
-
+ {/* Форма добавления сотрудника */} {showAddForm && ( @@ -443,602 +403,51 @@ ${employees.map((emp: Employee) => if (filteredEmployees.length === 0) { return ( -
-
-
- -
-

- {searchQuery ? 'Сотрудники не найдены' : 'У вас пока нет сотрудников'} -

-

- {searchQuery - ? 'Попробуйте изменить критерии поиска' - : 'Добавьте первого сотрудника в вашу команду' - } -

- {!searchQuery && ( - - )} -
-
+ setShowAddForm(true)} + /> ) } return (
{/* Навигация по месяцам */} -
-

Январь 2024

-
- - - -
-
+ - {/* Легенда точно как в гите */} -
-
-
- -
- Рабочий день -
-
-
- -
- Выходной -
-
-
- -
- Отпуск -
-
-
- -
- Больничный -
-
-
- -
- Прогул -
-
+ {/* Легенда статусов */} + {/* Объединенный список сотрудников с табелем */} - {filteredEmployees.map((employee: Employee) => { - // Генерируем календарные дни для текущего месяца - const currentDate = new Date() - const currentMonth = currentDate.getMonth() - const currentYear = currentDate.getFullYear() - const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate() - - // Создаем массив дней с моковыми данными табеля - const generateDayStatus = (day: number) => { - const date = new Date(currentYear, currentMonth, day) - const dayOfWeek = date.getDay() - - // Выходные - if (dayOfWeek === 0 || dayOfWeek === 6) return 'weekend' - - // Некоторые случайные отпуска/больничные для демонстрации - if ([15, 16].includes(day)) return 'vacation' - if ([10].includes(day)) return 'sick' - if ([22].includes(day)) return 'absent' - - return 'work' - } - - // const getDayClass = (status: string) => { - // switch (status) { - // case 'work': return 'bg-green-500/30 border-green-500/50' - // case 'weekend': return 'bg-gray-500/30 border-gray-500/50' - // case 'vacation': return 'bg-blue-500/30 border-blue-500/50' - // case 'sick': return 'bg-yellow-500/30 border-yellow-500/50' - // case 'absent': return 'bg-red-500/30 border-red-500/50' - // default: return 'bg-white/10 border-white/20' - // } - // } - - // Подсчитываем статистику - const stats = { - workDays: 0, - vacationDays: 0, - sickDays: 0, - absentDays: 0, - totalHours: 0 - } - - for (let day = 1; day <= daysInMonth; day++) { - const status = generateDayStatus(day) - switch (status) { - case 'work': - stats.workDays++ - stats.totalHours += 8 - break - case 'vacation': - stats.vacationDays++ - break - case 'sick': - stats.sickDays++ - break - case 'absent': - stats.absentDays++ - break - } - } - - return ( - -
- {/* Информация о сотруднике */} -
-
- - {employee.avatar ? ( - { - console.error('Ошибка загрузки аватара:', employee.avatar); - e.currentTarget.style.display = 'none'; - }} - onLoad={() => console.log('Аватар загружен успешно:', employee.avatar)} - /> - ) : null} - - {employee.firstName.charAt(0)}{employee.lastName.charAt(0)} - - - -
-
-

- {employee.firstName} {employee.lastName} -

-
- - - - - - - - - Уволить сотрудника? - - Вы уверены, что хотите уволить сотрудника {employee.firstName} {employee.lastName}? - Это действие нельзя отменить. - - - - - Отмена - - handleEmployeeDeleted(employee.id)} - disabled={deletingEmployeeId === employee.id} - className="bg-red-600 hover:bg-red-700 text-white" - > - {deletingEmployeeId === employee.id ? 'Увольнение...' : 'Уволить'} - - - - -
-
- -
-

- {employee.firstName} {employee.middleName} {employee.lastName} -

-

{employee.position}

- -
- -
- {/* Основные контакты */} -
- - {employee.phone} -
- {employee.email && ( -
- - {employee.email} -
- )} - - {/* Дата рождения */} - {employee.birthDate && ( -
- - - Родился: {new Date(employee.birthDate).toLocaleDateString('ru-RU')} - -
- )} - - {/* Дата приема на работу */} -
- - - Принят: {new Date(employee.hireDate).toLocaleDateString('ru-RU')} - -
- - {/* Адрес */} - {employee.address && ( -
- - {employee.address} -
- )} - - {/* Экстренный контакт */} - {employee.emergencyContact && ( -
- - - Экстр. контакт: {employee.emergencyContact} - {employee.emergencyPhone && ` (${employee.emergencyPhone})`} - + {filteredEmployees.map((employee: Employee) => ( + + ))}
- )} - - {/* Мессенджеры */} -
- {employee.telegram && ( -
- - @{employee.telegram} -
- )} - {employee.whatsapp && ( -
- - {employee.whatsapp} -
- )} -
-
-
-
- -
- - {/* Табель работы и статистика */} -
-

- - Табель работы за {new Date().toLocaleDateString('ru-RU', { month: 'long' })} -

- - {/* Сетка календаря - точно как в гите */} -
- {/* Заголовки дней недели */} - {['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => ( -
- {day} -
- ))} - - {/* Дни месяца */} - {(() => { - // Точная логика из гита - const calendarDays: (number | null)[] = [] - const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay() - const startOffset = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1 - - // Добавляем пустые ячейки для выравнивания первой недели - for (let i = 0; i < startOffset; i++) { - calendarDays.push(null) - } - - // Добавляем дни месяца - for (let day = 1; day <= daysInMonth; day++) { - calendarDays.push(day) - } - - // Функция для получения статуса дня из данных или дефолта - const getDayStatus = (day: number) => { - const date = new Date(currentYear, currentMonth, day) - const dateStr = date.toISOString().split('T')[0] // YYYY-MM-DD - - // Ищем запись в табеле для этого дня - const scheduleData = employeeSchedules[employee.id] || [] - const dayRecord = scheduleData.find(record => - record.date.split('T')[0] === dateStr - ) - - if (dayRecord) { - return dayRecord.status.toLowerCase() - } - - // Если записи нет, устанавливаем дефолтный статус - const dayOfWeek = date.getDay() - if (dayOfWeek === 0 || dayOfWeek === 6) return 'weekend' - return 'work' // По умолчанию рабочий день для новых сотрудников - } - - const getCellStyle = (status: string) => { - switch (status) { - case 'work': - return 'bg-emerald-500/20 text-emerald-300/70 border-emerald-400/80' - case 'weekend': - return 'bg-purple-500/20 text-purple-300/70 border-purple-400/80' - case 'vacation': - return 'bg-blue-500/20 text-blue-300/70 border-blue-400/80' - case 'sick': - return 'bg-yellow-500/20 text-yellow-300/70 border-yellow-400/80' - case 'absent': - return 'bg-red-500/20 text-red-300/70 border-red-400/80' - default: - return 'bg-white/10 text-white/50 border-white/20' - } - } - - const getStatusIcon = (status: string) => { - switch (status) { - case 'work': - return - case 'weekend': - return - case 'vacation': - return - case 'sick': - return - case 'absent': - return - default: - return null - } - } - - return calendarDays.map((day, index) => { - if (day === null) { - return
- } - - const status = getDayStatus(day) - const hours = status === 'work' ? 8 : status === 'vacation' || status === 'sick' ? 8 : 0 - const isToday = new Date().getDate() === day && - new Date().getMonth() === currentMonth && - new Date().getFullYear() === currentYear - - return ( -
{ - // Циклично переключаем статусы - changeDayStatus(employee.id, day, status) - }} - > -
-
- {getStatusIcon(status)} - {day} -
- {hours > 0 && ( - {hours}ч - )} -
- - {isToday && ( -
- )} -
- ) - }) - })()} -
- - {/* Статистика за месяц */} -
-
-

{stats.workDays}

-

Рабочих дней

-
-
-

{stats.vacationDays}

-

Отпуск

-
-
-

{stats.sickDays}

-

Больничный

-
-
-

{stats.totalHours}ч

-

Всего часов

-
-
-
-
-
- ) - })} -
) })()} - + - {employees.length === 0 ? ( - -
-
-
- -
-

Нет данных для отчетов

-

- Добавьте сотрудников, чтобы генерировать отчеты и аналитику -

- -
-
-
- ) : ( -
- {/* Статистические карточки */} -
- -
-
-

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

-

{employees.length}

-
- -
-
- -
-
-

Активных

-

- {employees.filter((e: Employee) => e.status === 'ACTIVE').length} -

-
- -
-
- -
-
-

Средняя зарплата

-

- {employees.length > 0 - ? Math.round(employees.reduce((sum: number, e: Employee) => sum + (e.salary || 0), 0) / employees.length).toLocaleString('ru-RU') - : '0'} ₽ -

-
- -
-
- -
-
-

Отделов

-

- {new Set(employees.map((e: Employee) => e.position).filter(Boolean)).size} -

-
- -
-
-
- - {/* Экспорт отчетов */} - -

- - Экспорт отчетов -

- -
-
-

Детальный отчет (CSV)

-

- Полная информация о всех сотрудниках в формате таблицы для Excel/Google Sheets -

- -
- -
-

Сводный отчет (TXT)

-

- Краткая статистика и список сотрудников в текстовом формате -

- -
-
-
- - {/* Аналитика по отделам */} - -

Распределение по отделам

-
- {Array.from(new Set(employees.map((e: Employee) => e.position).filter(Boolean)) as Set).map((position: string) => { - const positionEmployees = employees.filter((e: Employee) => e.position === position) - const percentage = Math.round((positionEmployees.length / employees.length) * 100) - - return ( -
-
-
- {position} - {positionEmployees.length} чел. ({percentage}%) -
-
-
-
-
-
- ) - })} -
-
-
- )} + setShowAddForm(true)} + onExportCSV={exportToCSV} + onGenerateReport={generateReport} + />
diff --git a/src/components/employees/month-navigation.tsx b/src/components/employees/month-navigation.tsx new file mode 100644 index 0000000..99018ae --- /dev/null +++ b/src/components/employees/month-navigation.tsx @@ -0,0 +1,32 @@ +"use client" + +import { Button } from '@/components/ui/button' + +interface MonthNavigationProps { + currentYear: number + currentMonth: number +} + +export function MonthNavigation({ currentYear, currentMonth }: MonthNavigationProps) { + const monthName = new Date(currentYear, currentMonth).toLocaleDateString('ru-RU', { + month: 'long', + year: 'numeric' + }) + + return ( +
+

{monthName}

+
+ + + +
+
+ ) +} \ No newline at end of file