Добавлены модели и функциональность для управления сотрудниками, включая создание, обновление и удаление сотрудников через GraphQL. Обновлены компоненты для отображения списка сотрудников и их расписания, улучшен интерфейс взаимодействия с пользователем. Реализованы функции экспорта отчетов в CSV и TXT форматах, добавлены новые интерфейсы и типы данных для сотрудников.

This commit is contained in:
Bivekich
2025-07-17 23:55:11 +03:00
parent 3e2a03da8c
commit d361364716
13 changed files with 3444 additions and 428 deletions

View File

@ -0,0 +1,453 @@
"use client"
import { useState, useRef } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Card } from '@/components/ui/card'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { Upload, X, User, Camera } from 'lucide-react'
import { toast } from 'sonner'
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 EmployeeFormProps {
employee?: Employee | null
onSave: (employeeData: Partial<Employee>) => void
onCancel: () => void
}
export function EmployeeForm({ employee, onSave, onCancel }: EmployeeFormProps) {
const [formData, setFormData] = useState({
firstName: employee?.firstName || '',
lastName: employee?.lastName || '',
middleName: employee?.middleName || '',
position: employee?.position || '',
phone: employee?.phone || '',
email: employee?.email || '',
avatar: employee?.avatar || '',
hireDate: employee?.hireDate || new Date().toISOString().split('T')[0],
status: employee?.status || 'ACTIVE' as const,
salary: employee?.salary || 0,
address: employee?.address || '',
birthDate: employee?.birthDate || '',
passportSeries: employee?.passportSeries || '',
passportNumber: employee?.passportNumber || '',
passportIssued: employee?.passportIssued || '',
passportDate: employee?.passportDate || '',
emergencyContact: employee?.emergencyContact || '',
emergencyPhone: employee?.emergencyPhone || ''
})
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const [loading, setLoading] = useState(false)
const handleInputChange = (field: string, value: string | number) => {
setFormData(prev => ({
...prev,
[field]: value
}))
}
const handleAvatarUpload = async (file: File) => {
setIsUploadingAvatar(true)
try {
const formDataUpload = new FormData()
formDataUpload.append('file', file)
formDataUpload.append('key', `avatars/employees/${Date.now()}-${file.name}`)
const response = await fetch('/api/upload-avatar', {
method: 'POST',
body: formDataUpload
})
if (!response.ok) {
throw new Error('Ошибка загрузки аватара')
}
const result = await response.json()
setFormData(prev => ({
...prev,
avatar: result.url
}))
toast.success('Аватар успешно загружен')
} catch (error) {
console.error('Error uploading avatar:', error)
toast.error('Ошибка при загрузке аватара')
} finally {
setIsUploadingAvatar(false)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
// Валидация
if (!formData.firstName || !formData.lastName || !formData.position) {
toast.error('Пожалуйста, заполните все обязательные поля')
setLoading(false)
return
}
if (formData.email && !/\S+@\S+\.\S+/.test(formData.email)) {
toast.error('Введите корректный email адрес')
setLoading(false)
return
}
if (formData.phone && !/^[\+]?[1-9][\d]{0,15}$/.test(formData.phone.replace(/\s/g, ''))) {
toast.error('Введите корректный номер телефона')
setLoading(false)
return
}
try {
// Для создания/обновления отправляем только нужные поля
const employeeData = {
firstName: formData.firstName,
lastName: formData.lastName,
middleName: formData.middleName,
position: formData.position,
phone: formData.phone,
email: formData.email || undefined,
avatar: formData.avatar || undefined,
hireDate: formData.hireDate,
salary: formData.salary || undefined,
address: formData.address || undefined,
birthDate: formData.birthDate || undefined,
passportSeries: formData.passportSeries || undefined,
passportNumber: formData.passportNumber || undefined,
passportIssued: formData.passportIssued || undefined,
passportDate: formData.passportDate || undefined,
emergencyContact: formData.emergencyContact || undefined,
emergencyPhone: formData.emergencyPhone || undefined
}
onSave(employeeData)
toast.success(employee ? 'Сотрудник успешно обновлен' : 'Сотрудник успешно добавлен')
} catch (error) {
console.error('Error saving employee:', error)
toast.error('Ошибка при сохранении данных сотрудника')
} finally {
setLoading(false)
}
}
const getInitials = () => {
const first = formData.firstName.charAt(0).toUpperCase()
const last = formData.lastName.charAt(0).toUpperCase()
return `${first}${last}`
}
const formatPhoneInput = (value: string) => {
const cleaned = value.replace(/\D/g, '')
if (cleaned.length <= 1) return cleaned
if (cleaned.length <= 4) return `+7 (${cleaned.slice(1)}`
if (cleaned.length <= 7) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4)}`
if (cleaned.length <= 9) return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`
return `+7 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 9)}-${cleaned.slice(9, 11)}`
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Фото и основная информация */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<h3 className="text-white font-medium mb-4">Личные данные</h3>
{/* Аватар */}
<div className="flex items-start gap-6 mb-6">
<div className="flex flex-col items-center gap-3">
<Avatar className="h-24 w-24 ring-2 ring-white/20">
{formData.avatar ? (
<AvatarImage src={formData.avatar} alt="Аватар сотрудника" />
) : null}
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-purple-600 text-white text-lg font-semibold">
{getInitials() || <User className="h-8 w-8" />}
</AvatarFallback>
</Avatar>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={(e) => e.target.files?.[0] && handleAvatarUpload(e.target.files[0])}
className="hidden"
/>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => fileInputRef.current?.click()}
disabled={isUploadingAvatar}
className="glass-secondary text-white hover:text-white"
>
<Camera className="h-4 w-4 mr-2" />
{isUploadingAvatar ? 'Загрузка...' : 'Изменить фото'}
</Button>
</div>
<div className="flex-1 grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">
Имя <span className="text-red-400">*</span>
</Label>
<Input
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
placeholder="Александр"
className="glass-input text-white placeholder:text-white/40 h-10"
required
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">
Фамилия <span className="text-red-400">*</span>
</Label>
<Input
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
placeholder="Петров"
className="glass-input text-white placeholder:text-white/40 h-10"
required
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Отчество</Label>
<Input
value={formData.middleName}
onChange={(e) => handleInputChange('middleName', e.target.value)}
placeholder="Иванович"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Дата рождения</Label>
<Input
type="date"
value={formData.birthDate}
onChange={(e) => handleInputChange('birthDate', e.target.value)}
className="glass-input text-white h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Серия паспорта</Label>
<Input
value={formData.passportSeries}
onChange={(e) => handleInputChange('passportSeries', e.target.value)}
placeholder="1234"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Номер паспорта</Label>
<Input
value={formData.passportNumber}
onChange={(e) => handleInputChange('passportNumber', e.target.value)}
placeholder="567890"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Кем выдан</Label>
<Input
value={formData.passportIssued}
onChange={(e) => handleInputChange('passportIssued', e.target.value)}
placeholder="ОУФМС России по г. Москве"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Дата выдачи</Label>
<Input
type="date"
value={formData.passportDate}
onChange={(e) => handleInputChange('passportDate', e.target.value)}
className="glass-input text-white h-10"
/>
</div>
</div>
</div>
<div className="mt-4">
<Label className="text-white/80 text-sm mb-2 block">Адрес проживания</Label>
<Input
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
placeholder="Москва, ул. Ленина, 10, кв. 5"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</Card>
{/* Рабочая информация */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<h3 className="text-white font-medium mb-4">Трудовая деятельность</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">
Должность <span className="text-red-400">*</span>
</Label>
<Input
value={formData.position}
onChange={(e) => handleInputChange('position', e.target.value)}
placeholder="Менеджер склада"
className="glass-input text-white placeholder:text-white/40 h-10"
required
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Дата приема на работу</Label>
<Input
type="date"
value={formData.hireDate}
onChange={(e) => handleInputChange('hireDate', e.target.value)}
className="glass-input text-white h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Статус</Label>
<Select
value={formData.status}
onValueChange={(value: 'active' | 'vacation' | 'sick' | 'inactive') => handleInputChange('status', value)}
>
<SelectTrigger className="glass-input text-white h-10">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-gray-900 border-white/20">
<SelectItem value="active" className="text-white hover:bg-white/10">Активен</SelectItem>
<SelectItem value="vacation" className="text-white hover:bg-white/10">В отпуске</SelectItem>
<SelectItem value="sick" className="text-white hover:bg-white/10">На больничном</SelectItem>
<SelectItem value="inactive" className="text-white hover:bg-white/10">Неактивен</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="mt-4">
<Label className="text-white/80 text-sm mb-2 block">Зарплата ()</Label>
<Input
type="number"
min="0"
value={formData.salary || ''}
onChange={(e) => handleInputChange('salary', parseInt(e.target.value) || 0)}
placeholder="80000"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</Card>
{/* Контактная информация */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<h3 className="text-white font-medium mb-4">Контактные данные</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">Телефон</Label>
<Input
value={formData.phone}
onChange={(e) => {
const formatted = formatPhoneInput(e.target.value)
handleInputChange('phone', formatted)
}}
placeholder="+7 (999) 123-45-67"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Email</Label>
<Input
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="a.petrov@company.com"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Экстренный контакт</Label>
<Input
value={formData.emergencyContact}
onChange={(e) => handleInputChange('emergencyContact', e.target.value)}
placeholder="ФИО близкого родственника"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Телефон экстренного контакта</Label>
<Input
value={formData.emergencyPhone}
onChange={(e) => {
const formatted = formatPhoneInput(e.target.value)
handleInputChange('emergencyPhone', formatted)
}}
placeholder="+7 (999) 123-45-67"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
</Card>
{/* Кнопки управления */}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 border-purple-400/30 text-purple-200 hover:bg-purple-500/10 hover:border-purple-300 transition-all duration-300"
>
Отмена
</Button>
<Button
type="submit"
disabled={loading || isUploadingAvatar}
className="flex-1 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"
>
{loading ? 'Сохранение...' : (employee ? 'Сохранить изменения' : 'Добавить сотрудника')}
</Button>
</div>
</form>
)
}