Обновлен компонент панели управления сотрудниками: добавлены новые компоненты для заголовка, поиска, легенды статусов и состояния пустого списка. Оптимизирована логика отображения сотрудников и их табелей. Удалены неиспользуемые импорты и код для улучшения читаемости.

This commit is contained in:
Bivekich
2025-07-30 15:40:49 +03:00
parent c99104c5ce
commit 84720a634d
11 changed files with 937 additions and 637 deletions

View File

@ -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 <CheckCircle className="h-3 w-3" />
case 'weekend':
return <Clock className="h-3 w-3" />
case 'vacation':
return <Plane className="h-3 w-3" />
case 'sick':
return <Activity className="h-3 w-3" />
case 'absent':
return <XCircle className="h-3 w-3" />
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 (
<div className="space-y-4">
<h4 className="text-white/80 font-medium mb-3 flex items-center gap-2">
<Calendar className="h-4 w-4" />
Табель работы за {new Date(currentYear, currentMonth).toLocaleDateString('ru-RU', { month: 'long' })}
</h4>
{/* Сетка календаря */}
<div className="grid grid-cols-7 gap-2">
{/* Заголовки дней недели */}
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => (
<div key={day} className="p-2 text-center text-white/70 font-medium text-sm">
{day}
</div>
))}
{/* Дни месяца */}
{calendarDays.map((day, index) => {
if (day === null) {
return <div key={`empty-${index}`} className="p-2"></div>
}
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 (
<div
key={`${employeeId}-${day}`}
className={`
relative p-2 min-h-[60px] border rounded-lg cursor-pointer
transition-transform duration-150 hover:scale-105 active:scale-95
${getCellStyle(status)}
${isToday ? 'ring-2 ring-white/50' : ''}
`}
onClick={() => onDayStatusChange(employeeId, day, status)}
>
<div className="flex flex-col items-center justify-center h-full">
<div className="flex items-center gap-1 mb-1">
{getStatusIcon(status)}
<span className="font-semibold text-sm">{day}</span>
</div>
{hours > 0 && (
<span className="text-xs opacity-80">{hours}ч</span>
)}
</div>
{isToday && (
<div className="absolute -top-1 -right-1 w-3 h-3 bg-white rounded-full"></div>
)}
</div>
)
})}
</div>
</div>
)
}

View File

@ -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 (
<div className="lg:w-80 flex-shrink-0">
<div className="flex items-start space-x-4 mb-4">
<Avatar className="h-16 w-16 ring-2 ring-white/20">
{employee.avatar ? (
<AvatarImage
src={employee.avatar}
alt={`${employee.firstName} ${employee.lastName}`}
onError={(e) => {
console.error('Ошибка загрузки аватара:', employee.avatar);
e.currentTarget.style.display = 'none';
}}
onLoad={() => console.log('Аватар загружен успешно:', employee.avatar)}
/>
) : null}
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-purple-600 text-white font-semibold text-lg">
{employee.firstName.charAt(0)}{employee.lastName.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-2">
<h3 className="text-white font-semibold text-lg truncate">
{employee.firstName} {employee.lastName}
</h3>
<div className="flex gap-1">
<Button
size="sm"
variant="ghost"
className="text-white/60 hover:text-white hover:bg-white/10 h-8 w-8 p-0"
onClick={() => onEdit(employee)}
>
<Edit className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="sm"
variant="ghost"
className="text-red-400/60 hover:text-red-300 hover:bg-red-500/10 h-8 w-8 p-0"
>
<UserX className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="glass-card border-white/10">
<AlertDialogHeader>
<AlertDialogTitle className="text-white">Уволить сотрудника?</AlertDialogTitle>
<AlertDialogDescription className="text-white/70">
Вы уверены, что хотите уволить сотрудника {employee.firstName} {employee.lastName}?
Это действие нельзя отменить.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="glass-secondary text-white hover:text-white">
Отмена
</AlertDialogCancel>
<AlertDialogAction
onClick={() => onDelete(employee.id)}
disabled={deletingEmployeeId === employee.id}
className="bg-red-600 hover:bg-red-700 text-white"
>
{deletingEmployeeId === employee.id ? 'Увольнение...' : 'Уволить'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="mb-3">
<h4 className="text-white font-semibold text-base">
{employee.firstName} {employee.middleName} {employee.lastName}
</h4>
<p className="text-purple-300 font-medium">{employee.position}</p>
</div>
<div className="space-y-2 text-sm">
{/* Основные контакты */}
<div className="flex items-center text-white/70">
<Phone className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">{employee.phone}</span>
</div>
{employee.email && (
<div className="flex items-center text-white/70">
<Mail className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">{employee.email}</span>
</div>
)}
{/* Дата рождения */}
{employee.birthDate && (
<div className="flex items-center text-white/70">
<Calendar className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">
Родился: {new Date(employee.birthDate).toLocaleDateString('ru-RU')}
</span>
</div>
)}
{/* Дата приема на работу */}
<div className="flex items-center text-white/70">
<Briefcase className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">
Принят: {new Date(employee.hireDate).toLocaleDateString('ru-RU')}
</span>
</div>
{/* Адрес */}
{employee.address && (
<div className="flex items-center text-white/70">
<MapPin className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">{employee.address}</span>
</div>
)}
{/* Экстренный контакт */}
{employee.emergencyContact && (
<div className="flex items-center text-white/70">
<AlertCircle className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">
Экстр. контакт: {employee.emergencyContact}
{employee.emergencyPhone && ` (${employee.emergencyPhone})`}
</span>
</div>
)}
{/* Мессенджеры */}
<div className="flex gap-2">
{employee.telegram && (
<div className="flex items-center text-blue-400">
<MessageCircle className="h-3 w-3 mr-1 flex-shrink-0" />
<span className="truncate text-xs">@{employee.telegram}</span>
</div>
)}
{employee.whatsapp && (
<div className="flex items-center text-green-400">
<Phone className="h-3 w-3 mr-1 flex-shrink-0" />
<span className="truncate text-xs">{employee.whatsapp}</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -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 (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="w-16 h-16 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-4">
<Users className="h-8 w-8 text-white/40" />
</div>
<h3 className="text-lg font-medium text-white mb-2">
{searchQuery ? 'Сотрудники не найдены' : 'У вас пока нет сотрудников'}
</h3>
<p className="text-white/60 text-sm mb-4">
{searchQuery
? 'Попробуйте изменить критерии поиска'
: 'Добавьте первого сотрудника в вашу команду'
}
</p>
{!searchQuery && (
<Button
onClick={onShowAddForm}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300"
>
<Plus className="h-4 w-4 mr-2" />
Добавить сотрудника
</Button>
)}
</div>
</div>
)
}

View File

@ -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 (
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Users className="h-8 w-8 text-purple-400" />
<div>
<h1 className="text-2xl font-bold text-white">Управление сотрудниками</h1>
<p className="text-white/70">Личные данные, табель работы и учет</p>
</div>
</div>
<Button
onClick={onToggleAddForm}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white border-0 shadow-lg shadow-purple-500/25 transition-all duration-300"
>
<Plus className="h-4 w-4 mr-2" />
{showAddForm ? 'Скрыть форму' : 'Добавить сотрудника'}
</Button>
</div>
)
}

View File

@ -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 (
<Card key={employee.id} className="glass-card p-6">
<div className="flex flex-col lg:flex-row gap-6">
{/* Информация о сотруднике */}
<EmployeeCard
employee={employee}
onEdit={onEdit}
onDelete={onDelete}
deletingEmployeeId={deletingEmployeeId}
/>
{/* Табель работы и статистика */}
<div className="flex-1 space-y-4">
<EmployeeCalendar
employeeId={employee.id}
employeeSchedules={employeeSchedules}
currentYear={currentYear}
currentMonth={currentMonth}
onDayStatusChange={onDayStatusChange}
/>
{/* Статистика за месяц */}
<EmployeeStats
currentYear={currentYear}
currentMonth={currentMonth}
/>
</div>
</div>
</Card>
)
}

View File

@ -0,0 +1,40 @@
"use client"
import { CheckCircle, Clock, Plane, Activity, XCircle } from 'lucide-react'
export function EmployeeLegend() {
return (
<div className="flex flex-wrap gap-3">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-emerald-500/20 text-emerald-300/70 border-emerald-400/80 flex items-center justify-center">
<CheckCircle className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Рабочий день</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-purple-500/20 text-purple-300/70 border-purple-400/80 flex items-center justify-center">
<Clock className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Выходной</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-blue-500/20 text-blue-300/70 border-blue-400/80 flex items-center justify-center">
<Plane className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Отпуск</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-yellow-500/20 text-yellow-300/70 border-yellow-400/80 flex items-center justify-center">
<Activity className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Больничный</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-red-500/20 text-red-300/70 border-red-400/80 flex items-center justify-center">
<XCircle className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Прогул</span>
</div>
</div>
)
}

View File

@ -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 (
<Card className="glass-card p-6">
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="w-16 h-16 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-4">
<BarChart3 className="h-8 w-8 text-white/40" />
</div>
<h3 className="text-lg font-medium text-white mb-2">Нет данных для отчетов</h3>
<p className="text-white/60 text-sm mb-4">
Добавьте сотрудников, чтобы генерировать отчеты и аналитику
</p>
<Button
onClick={onShowAddForm}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300"
>
<Plus className="h-4 w-4 mr-2" />
Добавить сотрудника
</Button>
</div>
</div>
</Card>
)
}
return (
<div className="space-y-6">
{/* Статистические карточки */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-white/70 text-sm">Всего сотрудников</p>
<p className="text-2xl font-bold text-white">{employees.length}</p>
</div>
<Users className="h-8 w-8 text-purple-400" />
</div>
</Card>
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-white/70 text-sm">Активных</p>
<p className="text-2xl font-bold text-green-400">
{employees.filter((e: Employee) => e.status === 'ACTIVE').length}
</p>
</div>
<BarChart3 className="h-8 w-8 text-green-400" />
</div>
</Card>
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-white/70 text-sm">Средняя зарплата</p>
<p className="text-2xl font-bold text-blue-400">
{employees.length > 0
? Math.round(employees.reduce((sum: number, e: Employee) => sum + (e.salary || 0), 0) / employees.length).toLocaleString('ru-RU')
: '0'}
</p>
</div>
<FileText className="h-8 w-8 text-blue-400" />
</div>
</Card>
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-white/70 text-sm">Отделов</p>
<p className="text-2xl font-bold text-orange-400">
{new Set(employees.map((e: Employee) => e.position).filter(Boolean)).size}
</p>
</div>
<Calendar className="h-8 w-8 text-orange-400" />
</div>
</Card>
</div>
{/* Экспорт отчетов */}
<Card className="glass-card p-6">
<h3 className="text-white font-medium mb-4 flex items-center gap-2">
<Download className="h-5 w-5" />
Экспорт отчетов
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-3">
<h4 className="text-white/80 font-medium">Детальный отчет (CSV)</h4>
<p className="text-white/60 text-sm">
Полная информация о всех сотрудниках в формате таблицы для Excel/Google Sheets
</p>
<Button
onClick={onExportCSV}
className="w-full glass-button"
>
<Download className="h-4 w-4 mr-2" />
Скачать CSV
</Button>
</div>
<div className="space-y-3">
<h4 className="text-white/80 font-medium">Сводный отчет (TXT)</h4>
<p className="text-white/60 text-sm">
Краткая статистика и список сотрудников в текстовом формате
</p>
<Button
onClick={onGenerateReport}
className="w-full glass-button"
>
<BarChart3 className="h-4 w-4 mr-2" />
Создать отчет
</Button>
</div>
</div>
</Card>
{/* Аналитика по отделам */}
<Card className="glass-card p-6">
<h3 className="text-white font-medium mb-4">Распределение по отделам</h3>
<div className="space-y-3">
{Array.from(new Set(employees.map((e: Employee) => e.position).filter(Boolean)) as Set<string>).map((position: string) => {
const positionEmployees = employees.filter((e: Employee) => e.position === position)
const percentage = Math.round((positionEmployees.length / employees.length) * 100)
return (
<div key={position} className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<span className="text-white/80 text-sm">{position}</span>
<span className="text-white/60 text-xs">{positionEmployees.length} чел. ({percentage}%)</span>
</div>
<div className="w-full bg-white/10 rounded-full h-2">
<div
className="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full"
style={{ width: `${percentage}%` }}
></div>
</div>
</div>
</div>
)
})}
</div>
</Card>
</div>
)
}

View File

@ -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 (
<Card className="glass-card p-4 mb-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/60 h-4 w-4" />
<Input
placeholder="Поиск сотрудников..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="glass-input pl-10"
/>
</div>
</Card>
)
}

View File

@ -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 (
<div className="grid grid-cols-4 gap-3 mt-4">
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-emerald-200 font-semibold text-lg">{stats.workDays}</p>
<p className="text-white/60 text-xs">Рабочих дней</p>
</div>
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-sky-200 font-semibold text-lg">{stats.vacationDays}</p>
<p className="text-white/60 text-xs">Отпуск</p>
</div>
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-amber-200 font-semibold text-lg">{stats.sickDays}</p>
<p className="text-white/60 text-xs">Больничный</p>
</div>
<div className="text-center p-3 bg-white/5 rounded-lg">
<p className="text-white font-semibold text-lg">{stats.totalHours}ч</p>
<p className="text-white/60 text-xs">Всего часов</p>
</div>
</div>
)
}

View File

@ -5,39 +5,20 @@ import { useQuery, useMutation } from '@apollo/client'
import { apolloClient } from '@/lib/apollo-client' import { apolloClient } from '@/lib/apollo-client'
import { Sidebar } from '@/components/dashboard/sidebar' import { Sidebar } from '@/components/dashboard/sidebar'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' 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 { EmployeeInlineForm } from './employee-inline-form'
import { EmployeeEditInlineForm } from './employee-edit-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 { 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 { GET_MY_EMPLOYEES, GET_EMPLOYEE_SCHEDULE } from '@/graphql/queries'
import { CREATE_EMPLOYEE, UPDATE_EMPLOYEE, DELETE_EMPLOYEE, UPDATE_EMPLOYEE_SCHEDULE } from '@/graphql/mutations' import { CREATE_EMPLOYEE, UPDATE_EMPLOYEE, DELETE_EMPLOYEE, UPDATE_EMPLOYEE_SCHEDULE } from '@/graphql/mutations'
import { import { Users, FileText } from 'lucide-react'
Users,
Calendar,
Search,
Plus,
FileText,
Edit,
UserX,
Phone,
Mail,
Download,
BarChart3,
CheckCircle,
XCircle,
Plane,
Activity,
Clock,
Briefcase,
MapPin,
AlertCircle,
MessageCircle
} from 'lucide-react'
// Интерфейс сотрудника // Интерфейс сотрудника
interface Employee { interface Employee {
@ -360,37 +341,16 @@ ${employees.map((emp: Employee) =>
<main className="flex-1 ml-56 p-6"> <main className="flex-1 ml-56 p-6">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
{/* Заголовок страницы */} {/* Заголовок страницы */}
<div className="flex items-center justify-between mb-6"> <EmployeeHeader
<div className="flex items-center gap-3"> showAddForm={showAddForm}
<Users className="h-8 w-8 text-purple-400" /> onToggleAddForm={() => setShowAddForm(!showAddForm)}
<div> />
<h1 className="text-2xl font-bold text-white">Управление сотрудниками</h1>
<p className="text-white/70">Личные данные, табель работы и учет</p>
</div>
</div>
<Button
onClick={() => setShowAddForm(!showAddForm)}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white border-0 shadow-lg shadow-purple-500/25 transition-all duration-300"
>
<Plus className="h-4 w-4 mr-2" />
{showAddForm ? 'Скрыть форму' : 'Добавить сотрудника'}
</Button>
</div>
{/* Поиск */} {/* Поиск */}
<Card className="glass-card p-4 mb-6"> <EmployeeSearch
<div className="relative"> searchQuery={searchQuery}
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/60 h-4 w-4" /> onSearchChange={setSearchQuery}
<Input
placeholder="Поиск сотрудников..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="glass-input pl-10"
/> />
</div>
</Card>
{/* Форма добавления сотрудника */} {/* Форма добавления сотрудника */}
{showAddForm && ( {showAddForm && (
@ -443,457 +403,38 @@ ${employees.map((emp: Employee) =>
if (filteredEmployees.length === 0) { if (filteredEmployees.length === 0) {
return ( return (
<div className="flex items-center justify-center h-64"> <EmployeeEmptyState
<div className="text-center"> searchQuery={searchQuery}
<div className="w-16 h-16 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-4"> onShowAddForm={() => setShowAddForm(true)}
<Users className="h-8 w-8 text-white/40" /> />
</div>
<h3 className="text-lg font-medium text-white mb-2">
{searchQuery ? 'Сотрудники не найдены' : 'У вас пока нет сотрудников'}
</h3>
<p className="text-white/60 text-sm mb-4">
{searchQuery
? 'Попробуйте изменить критерии поиска'
: 'Добавьте первого сотрудника в вашу команду'
}
</p>
{!searchQuery && (
<Button
onClick={() => setShowAddForm(true)}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300"
>
<Plus className="h-4 w-4 mr-2" />
Добавить сотрудника
</Button>
)}
</div>
</div>
) )
} }
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Навигация по месяцам */} {/* Навигация по месяцам */}
<div className="flex items-center justify-between mb-6"> <MonthNavigation
<h3 className="text-white font-medium text-lg">Январь 2024</h3> currentYear={currentYear}
<div className="flex gap-2"> currentMonth={currentMonth}
<Button variant="outline" size="sm" className="glass-secondary text-white hover:text-white"> />
Декабрь
</Button>
<Button variant="outline" size="sm" className="glass-secondary text-white hover:text-white">
Сегодня
</Button>
<Button variant="outline" size="sm" className="glass-secondary text-white hover:text-white">
Февраль
</Button>
</div>
</div>
{/* Легенда точно как в гите */} {/* Легенда статусов */}
<div className="flex flex-wrap gap-3"> <EmployeeLegend />
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-emerald-500/20 text-emerald-300/70 border-emerald-400/80 flex items-center justify-center">
<CheckCircle className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Рабочий день</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-purple-500/20 text-purple-300/70 border-purple-400/80 flex items-center justify-center">
<Clock className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Выходной</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-blue-500/20 text-blue-300/70 border-blue-400/80 flex items-center justify-center">
<Plane className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Отпуск</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-yellow-500/20 text-yellow-300/70 border-yellow-400/80 flex items-center justify-center">
<Activity className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Больничный</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border bg-red-500/20 text-red-300/70 border-red-400/80 flex items-center justify-center">
<XCircle className="h-3 w-3" />
</div>
<span className="text-white/70 text-sm">Прогул</span>
</div>
</div>
{/* Объединенный список сотрудников с табелем */} {/* Объединенный список сотрудников с табелем */}
{filteredEmployees.map((employee: Employee) => { {filteredEmployees.map((employee: Employee) => (
// Генерируем календарные дни для текущего месяца <EmployeeItem
const currentDate = new Date() key={employee.id}
const currentMonth = currentDate.getMonth() employee={employee}
const currentYear = currentDate.getFullYear() employeeSchedules={employeeSchedules}
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate() currentYear={currentYear}
currentMonth={currentMonth}
// Создаем массив дней с моковыми данными табеля onEdit={handleEditEmployee}
const generateDayStatus = (day: number) => { onDelete={handleEmployeeDeleted}
const date = new Date(currentYear, currentMonth, day) onDayStatusChange={changeDayStatus}
const dayOfWeek = date.getDay() deletingEmployeeId={deletingEmployeeId}
// Выходные
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 (
<Card key={employee.id} className="glass-card p-6">
<div className="flex flex-col lg:flex-row gap-6">
{/* Информация о сотруднике */}
<div className="lg:w-80 flex-shrink-0">
<div className="flex items-start space-x-4 mb-4">
<Avatar className="h-16 w-16 ring-2 ring-white/20">
{employee.avatar ? (
<AvatarImage
src={employee.avatar}
alt={`${employee.firstName} ${employee.lastName}`}
onError={(e) => {
console.error('Ошибка загрузки аватара:', employee.avatar);
e.currentTarget.style.display = 'none';
}}
onLoad={() => console.log('Аватар загружен успешно:', employee.avatar)}
/> />
) : null}
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-purple-600 text-white font-semibold text-lg">
{employee.firstName.charAt(0)}{employee.lastName.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-2">
<h3 className="text-white font-semibold text-lg truncate">
{employee.firstName} {employee.lastName}
</h3>
<div className="flex gap-1">
<Button
size="sm"
variant="ghost"
className="text-white/60 hover:text-white hover:bg-white/10 h-8 w-8 p-0"
onClick={() => handleEditEmployee(employee)}
>
<Edit className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="sm"
variant="ghost"
className="text-red-400/60 hover:text-red-300 hover:bg-red-500/10 h-8 w-8 p-0"
>
<UserX className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="glass-card border-white/10">
<AlertDialogHeader>
<AlertDialogTitle className="text-white">Уволить сотрудника?</AlertDialogTitle>
<AlertDialogDescription className="text-white/70">
Вы уверены, что хотите уволить сотрудника {employee.firstName} {employee.lastName}?
Это действие нельзя отменить.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="glass-secondary text-white hover:text-white">
Отмена
</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleEmployeeDeleted(employee.id)}
disabled={deletingEmployeeId === employee.id}
className="bg-red-600 hover:bg-red-700 text-white"
>
{deletingEmployeeId === employee.id ? 'Увольнение...' : 'Уволить'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="mb-3">
<h4 className="text-white font-semibold text-base">
{employee.firstName} {employee.middleName} {employee.lastName}
</h4>
<p className="text-purple-300 font-medium">{employee.position}</p>
</div>
<div className="space-y-2 text-sm">
{/* Основные контакты */}
<div className="flex items-center text-white/70">
<Phone className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">{employee.phone}</span>
</div>
{employee.email && (
<div className="flex items-center text-white/70">
<Mail className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">{employee.email}</span>
</div>
)}
{/* Дата рождения */}
{employee.birthDate && (
<div className="flex items-center text-white/70">
<Calendar className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">
Родился: {new Date(employee.birthDate).toLocaleDateString('ru-RU')}
</span>
</div>
)}
{/* Дата приема на работу */}
<div className="flex items-center text-white/70">
<Briefcase className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">
Принят: {new Date(employee.hireDate).toLocaleDateString('ru-RU')}
</span>
</div>
{/* Адрес */}
{employee.address && (
<div className="flex items-center text-white/70">
<MapPin className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">{employee.address}</span>
</div>
)}
{/* Экстренный контакт */}
{employee.emergencyContact && (
<div className="flex items-center text-white/70">
<AlertCircle className="h-3 w-3 mr-2 flex-shrink-0" />
<span className="truncate">
Экстр. контакт: {employee.emergencyContact}
{employee.emergencyPhone && ` (${employee.emergencyPhone})`}
</span>
</div>
)}
{/* Мессенджеры */}
<div className="flex gap-2">
{employee.telegram && (
<div className="flex items-center text-blue-400">
<MessageCircle className="h-3 w-3 mr-1 flex-shrink-0" />
<span className="truncate text-xs">@{employee.telegram}</span>
</div>
)}
{employee.whatsapp && (
<div className="flex items-center text-green-400">
<Phone className="h-3 w-3 mr-1 flex-shrink-0" />
<span className="truncate text-xs">{employee.whatsapp}</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* Табель работы и статистика */}
<div className="flex-1 space-y-4">
<h4 className="text-white/80 font-medium mb-3 flex items-center gap-2">
<Calendar className="h-4 w-4" />
Табель работы за {new Date().toLocaleDateString('ru-RU', { month: 'long' })}
</h4>
{/* Сетка календаря - точно как в гите */}
<div className="grid grid-cols-7 gap-2">
{/* Заголовки дней недели */}
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => (
<div key={day} className="p-2 text-center text-white/70 font-medium text-sm">
{day}
</div>
))} ))}
{/* Дни месяца */}
{(() => {
// Точная логика из гита
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 <CheckCircle className="h-3 w-3" />
case 'weekend':
return <Clock className="h-3 w-3" />
case 'vacation':
return <Plane className="h-3 w-3" />
case 'sick':
return <Activity className="h-3 w-3" />
case 'absent':
return <XCircle className="h-3 w-3" />
default:
return null
}
}
return calendarDays.map((day, index) => {
if (day === null) {
return <div key={`empty-${index}`} className="p-2"></div>
}
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 (
<div
key={`${employee.id}-${day}`}
className={`
relative p-2 min-h-[60px] border rounded-lg cursor-pointer
transition-transform duration-150 hover:scale-105 active:scale-95
${getCellStyle(status)}
${isToday ? 'ring-2 ring-white/50' : ''}
`}
onClick={() => {
// Циклично переключаем статусы
changeDayStatus(employee.id, day, status)
}}
>
<div className="flex flex-col items-center justify-center h-full">
<div className="flex items-center gap-1 mb-1">
{getStatusIcon(status)}
<span className="font-semibold text-sm">{day}</span>
</div>
{hours > 0 && (
<span className="text-xs opacity-80">{hours}ч</span>
)}
</div>
{isToday && (
<div className="absolute -top-1 -right-1 w-3 h-3 bg-white rounded-full"></div>
)}
</div>
)
})
})()}
</div>
{/* Статистика за месяц */}
<div className="grid grid-cols-4 gap-3 mt-4">
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-emerald-200 font-semibold text-lg">{stats.workDays}</p>
<p className="text-white/60 text-xs">Рабочих дней</p>
</div>
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-sky-200 font-semibold text-lg">{stats.vacationDays}</p>
<p className="text-white/60 text-xs">Отпуск</p>
</div>
<div className="text-center p-3 bg-white/10 rounded-lg">
<p className="text-amber-200 font-semibold text-lg">{stats.sickDays}</p>
<p className="text-white/60 text-xs">Больничный</p>
</div>
<div className="text-center p-3 bg-white/5 rounded-lg">
<p className="text-white font-semibold text-lg">{stats.totalHours}ч</p>
<p className="text-white/60 text-xs">Всего часов</p>
</div>
</div>
</div>
</div>
</Card>
)
})}
</div> </div>
) )
})()} })()}
@ -901,144 +442,12 @@ ${employees.map((emp: Employee) =>
</TabsContent> </TabsContent>
<TabsContent value="reports"> <TabsContent value="reports">
{employees.length === 0 ? ( <EmployeeReports
<Card className="glass-card p-6"> employees={employees}
<div className="flex items-center justify-center h-64"> onShowAddForm={() => setShowAddForm(true)}
<div className="text-center"> onExportCSV={exportToCSV}
<div className="w-16 h-16 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-4"> onGenerateReport={generateReport}
<BarChart3 className="h-8 w-8 text-white/40" /> />
</div>
<h3 className="text-lg font-medium text-white mb-2">Нет данных для отчетов</h3>
<p className="text-white/60 text-sm mb-4">
Добавьте сотрудников, чтобы генерировать отчеты и аналитику
</p>
<Button
onClick={() => setShowAddForm(true)}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300"
>
<Plus className="h-4 w-4 mr-2" />
Добавить сотрудника
</Button>
</div>
</div>
</Card>
) : (
<div className="space-y-6">
{/* Статистические карточки */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-white/70 text-sm">Всего сотрудников</p>
<p className="text-2xl font-bold text-white">{employees.length}</p>
</div>
<Users className="h-8 w-8 text-purple-400" />
</div>
</Card>
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-white/70 text-sm">Активных</p>
<p className="text-2xl font-bold text-green-400">
{employees.filter((e: Employee) => e.status === 'ACTIVE').length}
</p>
</div>
<BarChart3 className="h-8 w-8 text-green-400" />
</div>
</Card>
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-white/70 text-sm">Средняя зарплата</p>
<p className="text-2xl font-bold text-blue-400">
{employees.length > 0
? Math.round(employees.reduce((sum: number, e: Employee) => sum + (e.salary || 0), 0) / employees.length).toLocaleString('ru-RU')
: '0'}
</p>
</div>
<FileText className="h-8 w-8 text-blue-400" />
</div>
</Card>
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-white/70 text-sm">Отделов</p>
<p className="text-2xl font-bold text-orange-400">
{new Set(employees.map((e: Employee) => e.position).filter(Boolean)).size}
</p>
</div>
<Calendar className="h-8 w-8 text-orange-400" />
</div>
</Card>
</div>
{/* Экспорт отчетов */}
<Card className="glass-card p-6">
<h3 className="text-white font-medium mb-4 flex items-center gap-2">
<Download className="h-5 w-5" />
Экспорт отчетов
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-3">
<h4 className="text-white/80 font-medium">Детальный отчет (CSV)</h4>
<p className="text-white/60 text-sm">
Полная информация о всех сотрудниках в формате таблицы для Excel/Google Sheets
</p>
<Button
onClick={exportToCSV}
className="w-full glass-button"
>
<Download className="h-4 w-4 mr-2" />
Скачать CSV
</Button>
</div>
<div className="space-y-3">
<h4 className="text-white/80 font-medium">Сводный отчет (TXT)</h4>
<p className="text-white/60 text-sm">
Краткая статистика и список сотрудников в текстовом формате
</p>
<Button
onClick={generateReport}
className="w-full glass-button"
>
<BarChart3 className="h-4 w-4 mr-2" />
Создать отчет
</Button>
</div>
</div>
</Card>
{/* Аналитика по отделам */}
<Card className="glass-card p-6">
<h3 className="text-white font-medium mb-4">Распределение по отделам</h3>
<div className="space-y-3">
{Array.from(new Set(employees.map((e: Employee) => e.position).filter(Boolean)) as Set<string>).map((position: string) => {
const positionEmployees = employees.filter((e: Employee) => e.position === position)
const percentage = Math.round((positionEmployees.length / employees.length) * 100)
return (
<div key={position} className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<span className="text-white/80 text-sm">{position}</span>
<span className="text-white/60 text-xs">{positionEmployees.length} чел. ({percentage}%)</span>
</div>
<div className="w-full bg-white/10 rounded-full h-2">
<div
className="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full"
style={{ width: `${percentage}%` }}
></div>
</div>
</div>
</div>
)
})}
</div>
</Card>
</div>
)}
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@ -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 (
<div className="flex items-center justify-between mb-6">
<h3 className="text-white font-medium text-lg capitalize">{monthName}</h3>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="glass-secondary text-white hover:text-white">
Предыдущий
</Button>
<Button variant="outline" size="sm" className="glass-secondary text-white hover:text-white">
Сегодня
</Button>
<Button variant="outline" size="sm" className="glass-secondary text-white hover:text-white">
Следующий
</Button>
</div>
</div>
)
}