Удалены неиспользуемые импорты и функции из компонентов, улучшены стили и функциональность. Обновлены компоненты для работы с изображениями, добавлены новые интерфейсы и типы данных для сотрудников. Реализована логика загрузки расписания сотрудников через GraphQL, улучшен интерфейс взаимодействия с пользователем.
This commit is contained in:
@ -155,7 +155,6 @@ export function CartItems({ cart }: CartItemsProps) {
|
||||
}
|
||||
|
||||
const unavailableItems = cart.items.filter(item => !item.isAvailable)
|
||||
const availableItems = cart.items.filter(item => item.isAvailable)
|
||||
|
||||
// Группировка товаров по поставщикам
|
||||
const groupedItems = cart.items.reduce((groups, item) => {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
ShoppingCart,
|
||||
@ -131,7 +131,7 @@ export function CartSummary({ cart }: CartSummaryProps) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{Object.values(sellerGroups).map((group, index) => (
|
||||
{Object.values(sellerGroups).map((group) => (
|
||||
<div
|
||||
key={group.organization.id}
|
||||
className="bg-white/5 p-3 rounded-lg text-xs"
|
||||
|
@ -18,22 +18,7 @@ export function DashboardHome() {
|
||||
return 'Вашей организации'
|
||||
}
|
||||
|
||||
const getCabinetType = () => {
|
||||
if (!user?.organization?.type) return 'кабинета'
|
||||
|
||||
switch (user.organization.type) {
|
||||
case 'FULFILLMENT':
|
||||
return 'фулфилмент кабинета'
|
||||
case 'SELLER':
|
||||
return 'селлер кабинета'
|
||||
case 'LOGIST':
|
||||
return 'логистического кабинета'
|
||||
case 'WHOLESALE':
|
||||
return 'оптового кабинета'
|
||||
default:
|
||||
return 'кабинета'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-smooth flex">
|
||||
|
@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useState, useRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@ -289,9 +290,11 @@ export function EmployeeEditInlineForm({ employee, onSave, onCancel, isLoading =
|
||||
<div className="space-y-3">
|
||||
{formData.passportPhoto ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
<Image
|
||||
src={formData.passportPhoto}
|
||||
alt="Паспорт"
|
||||
width={400}
|
||||
height={300}
|
||||
className="w-full h-auto max-h-48 object-contain rounded-lg border border-white/20 bg-white/5 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => setShowPassportPreview(true)}
|
||||
/>
|
||||
@ -546,9 +549,11 @@ export function EmployeeEditInlineForm({ employee, onSave, onCancel, isLoading =
|
||||
<DialogTitle className="text-white">Фото паспорта</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-center">
|
||||
<img
|
||||
src={formData.passportPhoto}
|
||||
alt="Паспорт"
|
||||
<Image
|
||||
src={formData.passportPhoto}
|
||||
alt="Паспорт"
|
||||
width={600}
|
||||
height={800}
|
||||
className="max-w-full max-h-[70vh] object-contain rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@ 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 { User, Camera } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface Employee {
|
||||
|
@ -1,12 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import {
|
||||
@ -19,7 +20,7 @@ import {
|
||||
Mail,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
Calendar,
|
||||
|
||||
FileText,
|
||||
MessageCircle
|
||||
} from 'lucide-react'
|
||||
@ -251,9 +252,11 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
<div className="space-y-3">
|
||||
{formData.passportPhoto ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
<Image
|
||||
src={formData.passportPhoto}
|
||||
alt="Паспорт"
|
||||
alt="Паспорт"
|
||||
width={400}
|
||||
height={300}
|
||||
className="w-full h-auto max-h-48 object-contain rounded-lg border border-white/20 bg-white/5 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => setShowPassportPreview(true)}
|
||||
/>
|
||||
@ -508,9 +511,11 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
||||
<DialogTitle className="text-white">Фото паспорта</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-center">
|
||||
<img
|
||||
src={formData.passportPhoto}
|
||||
alt="Паспорт"
|
||||
<Image
|
||||
src={formData.passportPhoto}
|
||||
alt="Паспорт"
|
||||
width={600}
|
||||
height={800}
|
||||
className="max-w-full max-h-[70vh] object-contain rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
@ -4,13 +4,13 @@ import { useState } from 'react'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import {
|
||||
Calendar,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
User,
|
||||
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Plane,
|
||||
@ -164,17 +164,17 @@ export function EmployeeSchedule({ employees }: EmployeeScheduleProps) {
|
||||
const getCellStyle = (status: WorkDayStatus) => {
|
||||
switch (status) {
|
||||
case 'work':
|
||||
return 'bg-green-500/20 text-green-300 border-green-500/30'
|
||||
return 'bg-emerald-500/20 text-emerald-300/70 border-emerald-400/80'
|
||||
case 'weekend':
|
||||
return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
return 'bg-purple-500/20 text-purple-300/70 border-purple-400/80'
|
||||
case 'vacation':
|
||||
return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
|
||||
return 'bg-blue-500/20 text-blue-300/70 border-blue-400/80'
|
||||
case 'sick':
|
||||
return 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30'
|
||||
return 'bg-yellow-500/20 text-yellow-300/70 border-yellow-400/80'
|
||||
case 'absent':
|
||||
return 'bg-red-500/20 text-red-300 border-red-500/30'
|
||||
return 'bg-red-500/20 text-red-300/70 border-red-400/80'
|
||||
default:
|
||||
return 'bg-white/10 text-white/70 border-white/20'
|
||||
return 'bg-white/10 text-white/50 border-white/20'
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,16 +197,16 @@ export function EmployeeSchedule({ employees }: EmployeeScheduleProps) {
|
||||
}
|
||||
|
||||
// Получить название статуса
|
||||
const getStatusName = (status: WorkDayStatus) => {
|
||||
switch (status) {
|
||||
case 'work': return 'Рабочий'
|
||||
case 'weekend': return 'Выходной'
|
||||
case 'vacation': return 'Отпуск'
|
||||
case 'sick': return 'Больничный'
|
||||
case 'absent': return 'Прогул'
|
||||
default: return 'Неизвестно'
|
||||
}
|
||||
}
|
||||
// const getStatusName = (status: WorkDayStatus) => {
|
||||
// switch (status) {
|
||||
// case 'work': return 'Рабочий'
|
||||
// case 'weekend': return 'Выходной'
|
||||
// case 'vacation': return 'Отпуск'
|
||||
// case 'sick': return 'Больничный'
|
||||
// case 'absent': return 'Прогул'
|
||||
// default: return 'Неизвестно'
|
||||
// }
|
||||
// }
|
||||
|
||||
// Создаем массив дней для отображения
|
||||
const calendarDays: (number | null)[] = []
|
||||
@ -224,8 +224,8 @@ export function EmployeeSchedule({ employees }: EmployeeScheduleProps) {
|
||||
|
||||
// Получить статистику для сотрудника за месяц
|
||||
const getMonthStats = (employeeId: string) => {
|
||||
const monthKey = `${currentYear}-${currentMonth}`
|
||||
const schedule = schedules[`${employeeId}-${monthKey}`]
|
||||
// const monthKey = `${currentYear}-${currentMonth}`
|
||||
// const schedule = schedules[`${employeeId}-${monthKey}`]
|
||||
|
||||
let workDays = 0
|
||||
let vacationDays = 0
|
||||
|
@ -1,20 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
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 { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { EmployeeForm } from './employee-form'
|
||||
// import { EmployeeForm } from './employee-form'
|
||||
import { EmployeeInlineForm } from './employee-inline-form'
|
||||
import { EmployeeEditInlineForm } from './employee-edit-inline-form'
|
||||
import { toast } from 'sonner'
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'
|
||||
import { GET_MY_EMPLOYEES } 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 {
|
||||
Users,
|
||||
@ -32,7 +32,11 @@ import {
|
||||
XCircle,
|
||||
Plane,
|
||||
Activity,
|
||||
Clock
|
||||
Clock,
|
||||
Briefcase,
|
||||
MapPin,
|
||||
AlertCircle,
|
||||
MessageCircle
|
||||
} from 'lucide-react'
|
||||
|
||||
// Интерфейс сотрудника
|
||||
@ -70,16 +74,63 @@ export function EmployeesDashboard() {
|
||||
const [createLoading, setCreateLoading] = useState(false)
|
||||
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null)
|
||||
const [deletingEmployeeId, setDeletingEmployeeId] = useState<string | null>(null)
|
||||
const [employeeSchedules, setEmployeeSchedules] = useState<{[key: string]: ScheduleRecord[]}>({})
|
||||
const [currentYear] = useState(new Date().getFullYear())
|
||||
const [currentMonth] = useState(new Date().getMonth())
|
||||
|
||||
interface ScheduleRecord {
|
||||
id: string
|
||||
date: string
|
||||
status: string
|
||||
hoursWorked?: number
|
||||
employee: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
// GraphQL запросы и мутации
|
||||
const { data, loading, refetch } = useQuery(GET_MY_EMPLOYEES)
|
||||
const [createEmployee] = useMutation(CREATE_EMPLOYEE)
|
||||
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE)
|
||||
const [deleteEmployee] = useMutation(DELETE_EMPLOYEE)
|
||||
const [updateEmployeeSchedule] = useMutation(UPDATE_EMPLOYEE_SCHEDULE)
|
||||
|
||||
const employees = data?.myEmployees || []
|
||||
const employees = useMemo(() => data?.myEmployees || [], [data?.myEmployees])
|
||||
|
||||
// Загружаем данные табеля для всех сотрудников
|
||||
useEffect(() => {
|
||||
const loadScheduleData = async () => {
|
||||
if (employees.length > 0) {
|
||||
const schedulePromises = employees.map(async (employee: Employee) => {
|
||||
try {
|
||||
const { data } = await apolloClient.query({
|
||||
query: GET_EMPLOYEE_SCHEDULE,
|
||||
variables: {
|
||||
employeeId: employee.id,
|
||||
year: currentYear,
|
||||
month: currentMonth
|
||||
}
|
||||
})
|
||||
return { employeeId: employee.id, scheduleData: data?.employeeSchedule || [] }
|
||||
} catch (error) {
|
||||
console.error(`Error loading schedule for ${employee.id}:`, error)
|
||||
return { employeeId: employee.id, scheduleData: [] }
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(schedulePromises)
|
||||
const scheduleMap: {[key: string]: ScheduleRecord[]} = {}
|
||||
results.forEach((result: { employeeId: string; scheduleData: ScheduleRecord[] }) => {
|
||||
if (result && result.scheduleData) {
|
||||
scheduleMap[result.employeeId] = result.scheduleData
|
||||
}
|
||||
})
|
||||
setEmployeeSchedules(scheduleMap)
|
||||
}
|
||||
}
|
||||
|
||||
loadScheduleData()
|
||||
}, [employees, currentYear, currentMonth])
|
||||
|
||||
const handleEditEmployee = (employee: Employee) => {
|
||||
setEditingEmployee(employee)
|
||||
@ -156,6 +207,72 @@ export function EmployeesDashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для изменения статуса дня в табеле
|
||||
const changeDayStatus = async (employeeId: string, day: number, currentStatus: string) => {
|
||||
try {
|
||||
// Циклично переключаем статусы
|
||||
const statuses = ['WORK', 'WEEKEND', 'VACATION', 'SICK', 'ABSENT']
|
||||
const currentIndex = statuses.indexOf(currentStatus.toUpperCase())
|
||||
const nextStatus = statuses[(currentIndex + 1) % statuses.length]
|
||||
|
||||
// Формируем дату
|
||||
const date = new Date(currentYear, currentMonth, day)
|
||||
const hours = nextStatus === 'WORK' ? 8 : 0
|
||||
|
||||
// Отправляем мутацию
|
||||
await updateEmployeeSchedule({
|
||||
variables: {
|
||||
input: {
|
||||
employeeId: employeeId,
|
||||
date: date.toISOString().split('T')[0], // YYYY-MM-DD формат
|
||||
status: nextStatus,
|
||||
hoursWorked: hours
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Обновляем локальное состояние
|
||||
const updatedDate = new Date(currentYear, currentMonth, day)
|
||||
const dateStr = updatedDate.toISOString().split('T')[0]
|
||||
|
||||
setEmployeeSchedules(prev => {
|
||||
const currentSchedule = prev[employeeId] || []
|
||||
const existingRecordIndex = currentSchedule.findIndex(record =>
|
||||
record.date.split('T')[0] === dateStr
|
||||
)
|
||||
|
||||
const newRecord = {
|
||||
id: Date.now().toString(), // временный ID
|
||||
date: updatedDate.toISOString(),
|
||||
status: nextStatus,
|
||||
hoursWorked: hours,
|
||||
employee: { id: employeeId }
|
||||
}
|
||||
|
||||
let updatedSchedule
|
||||
if (existingRecordIndex >= 0) {
|
||||
// Обновляем существующую запись
|
||||
updatedSchedule = [...currentSchedule]
|
||||
updatedSchedule[existingRecordIndex] = { ...updatedSchedule[existingRecordIndex], ...newRecord }
|
||||
} else {
|
||||
// Добавляем новую запись
|
||||
updatedSchedule = [...currentSchedule, newRecord]
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[employeeId]: updatedSchedule
|
||||
}
|
||||
})
|
||||
|
||||
toast.success('Статус дня обновлен')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating day status:', error)
|
||||
toast.error('Ошибка при обновлении статуса дня')
|
||||
}
|
||||
}
|
||||
|
||||
const exportToCSV = () => {
|
||||
const csvContent = [
|
||||
['ФИО', 'Должность', 'Статус', 'Зарплата', 'Телефон', 'Email', 'Дата найма'],
|
||||
@ -319,10 +436,9 @@ ${employees.map((emp: Employee) =>
|
||||
<TabsContent value="combined">
|
||||
<Card className="glass-card p-6">
|
||||
{(() => {
|
||||
const filteredEmployees = employees.filter(employee =>
|
||||
const filteredEmployees = employees.filter((employee: Employee) =>
|
||||
`${employee.firstName} ${employee.lastName}`.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
employee.position.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(employee.department && employee.department.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
employee.position.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
if (filteredEmployees.length === 0) {
|
||||
@ -376,31 +492,31 @@ ${employees.map((emp: Employee) =>
|
||||
{/* Легенда точно как в гите */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded border bg-green-500/20 text-green-300 border-green-500/30">
|
||||
<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-gray-500/20 text-gray-300 border-gray-500/30">
|
||||
<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 border-blue-500/30">
|
||||
<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 border-yellow-500/30">
|
||||
<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 border-red-500/30">
|
||||
<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>
|
||||
@ -408,7 +524,7 @@ ${employees.map((emp: Employee) =>
|
||||
</div>
|
||||
|
||||
{/* Объединенный список сотрудников с табелем */}
|
||||
{filteredEmployees.map((employee) => {
|
||||
{filteredEmployees.map((employee: Employee) => {
|
||||
// Генерируем календарные дни для текущего месяца
|
||||
const currentDate = new Date()
|
||||
const currentMonth = currentDate.getMonth()
|
||||
@ -431,16 +547,16 @@ ${employees.map((emp: Employee) =>
|
||||
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 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 = {
|
||||
@ -471,7 +587,7 @@ ${employees.map((emp: Employee) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<Card key={employee.id} className="glass-card p-6">
|
||||
<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">
|
||||
@ -489,34 +605,34 @@ ${employees.map((emp: Employee) =>
|
||||
/>
|
||||
) : 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)}
|
||||
{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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
@ -540,52 +656,90 @@ ${employees.map((emp: Employee) =>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-purple-300 font-medium mb-1">{employee.position}</p>
|
||||
{employee.department && (
|
||||
<p className="text-white/60 text-sm mb-3">{employee.department}</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center text-white/70 text-sm">
|
||||
<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 text-sm">
|
||||
<Mail className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span className="truncate">{employee.email}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Статистика за месяц */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="text-center p-3 bg-white/5 rounded-lg">
|
||||
<p className="text-green-400 font-semibold text-lg">{stats.workDays}</p>
|
||||
<p className="text-white/60 text-xs">Рабочих дней</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-white/5 rounded-lg">
|
||||
<p className="text-blue-400 font-semibold text-lg">{stats.vacationDays}</p>
|
||||
<p className="text-white/60 text-xs">Отпуск</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-white/5 rounded-lg">
|
||||
<p className="text-yellow-400 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 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>
|
||||
|
||||
{/* Табель работы 1 в 1 как в гите */}
|
||||
<div className="flex-1">
|
||||
</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' })}
|
||||
@ -597,7 +751,7 @@ ${employees.map((emp: Employee) =>
|
||||
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => (
|
||||
<div key={day} className="p-2 text-center text-white/70 font-medium text-sm">
|
||||
{day}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Дни месяца */}
|
||||
@ -617,31 +771,41 @@ ${employees.map((emp: Employee) =>
|
||||
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'
|
||||
if ([15, 16].includes(day)) return 'vacation'
|
||||
if ([10].includes(day)) return 'sick'
|
||||
if ([22].includes(day)) return 'absent'
|
||||
return 'work'
|
||||
return 'work' // По умолчанию рабочий день для новых сотрудников
|
||||
}
|
||||
|
||||
const getCellStyle = (status: string) => {
|
||||
switch (status) {
|
||||
case 'work':
|
||||
return 'bg-green-500/20 text-green-300 border-green-500/30'
|
||||
return 'bg-emerald-500/20 text-emerald-300/70 border-emerald-400/80'
|
||||
case 'weekend':
|
||||
return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
return 'bg-purple-500/20 text-purple-300/70 border-purple-400/80'
|
||||
case 'vacation':
|
||||
return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
|
||||
return 'bg-blue-500/20 text-blue-300/70 border-blue-400/80'
|
||||
case 'sick':
|
||||
return 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30'
|
||||
return 'bg-yellow-500/20 text-yellow-300/70 border-yellow-400/80'
|
||||
case 'absent':
|
||||
return 'bg-red-500/20 text-red-300 border-red-500/30'
|
||||
return 'bg-red-500/20 text-red-300/70 border-red-400/80'
|
||||
default:
|
||||
return 'bg-white/10 text-white/70 border-white/20'
|
||||
return 'bg-white/10 text-white/50 border-white/20'
|
||||
}
|
||||
}
|
||||
|
||||
@ -678,16 +842,13 @@ ${employees.map((emp: Employee) =>
|
||||
key={`${employee.id}-${day}`}
|
||||
className={`
|
||||
relative p-2 min-h-[60px] border rounded-lg cursor-pointer
|
||||
transition-all duration-200 hover:scale-105
|
||||
transition-transform duration-150 hover:scale-105 active:scale-95
|
||||
${getCellStyle(status)}
|
||||
${isToday ? 'ring-2 ring-white/50' : ''}
|
||||
`}
|
||||
onClick={() => {
|
||||
// Циклично переключаем статусы - точно как в гите
|
||||
// const statuses = ['work', 'weekend', 'vacation', 'sick', 'absent']
|
||||
// const currentIndex = statuses.indexOf(status)
|
||||
// const nextStatus = statuses[(currentIndex + 1) % statuses.length]
|
||||
// changeDayStatus(employee.id, day, nextStatus)
|
||||
// Циклично переключаем статусы
|
||||
changeDayStatus(employee.id, day, status)
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
@ -698,8 +859,8 @@ ${employees.map((emp: Employee) =>
|
||||
{hours > 0 && (
|
||||
<span className="text-xs opacity-80">{hours}ч</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{isToday && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-white rounded-full"></div>
|
||||
)}
|
||||
@ -708,15 +869,35 @@ ${employees.map((emp: Employee) =>
|
||||
})
|
||||
})()}
|
||||
</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>
|
||||
)
|
||||
})()}
|
||||
</Card>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="reports">
|
||||
@ -783,7 +964,7 @@ ${employees.map((emp: Employee) =>
|
||||
<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.department).filter(Boolean)).size}
|
||||
{new Set(employees.map((e: Employee) => e.position).filter(Boolean)).size}
|
||||
</p>
|
||||
</div>
|
||||
<Calendar className="h-8 w-8 text-orange-400" />
|
||||
@ -833,16 +1014,16 @@ ${employees.map((emp: Employee) =>
|
||||
<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 => e.department).filter(Boolean))).map(dept => {
|
||||
const deptEmployees = employees.filter(e => e.department === dept)
|
||||
const percentage = Math.round((deptEmployees.length / employees.length) * 100)
|
||||
{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={dept} className="flex items-center justify-between">
|
||||
<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">{dept}</span>
|
||||
<span className="text-white/60 text-xs">{deptEmployees.length} чел. ({percentage}%)</span>
|
||||
<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
|
||||
|
@ -5,7 +5,7 @@ import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@ -20,7 +20,7 @@ import {
|
||||
Briefcase,
|
||||
Save,
|
||||
X,
|
||||
Trash2,
|
||||
|
||||
UserX
|
||||
} from 'lucide-react'
|
||||
|
||||
@ -40,61 +40,7 @@ interface Employee {
|
||||
address: string
|
||||
}
|
||||
|
||||
// Моковые данные сотрудников
|
||||
const mockEmployees: Employee[] = [
|
||||
{
|
||||
id: '1',
|
||||
firstName: 'Александр',
|
||||
lastName: 'Петров',
|
||||
position: 'Менеджер склада',
|
||||
department: 'Логистика',
|
||||
phone: '+7 (999) 123-45-67',
|
||||
email: 'a.petrov@company.com',
|
||||
hireDate: '2023-01-15',
|
||||
status: 'active',
|
||||
salary: 80000,
|
||||
address: 'Москва, ул. Ленина, 10'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
firstName: 'Мария',
|
||||
lastName: 'Иванова',
|
||||
position: 'Кладовщик',
|
||||
department: 'Логистика',
|
||||
phone: '+7 (999) 234-56-78',
|
||||
email: 'm.ivanova@company.com',
|
||||
hireDate: '2023-03-20',
|
||||
status: 'active',
|
||||
salary: 60000,
|
||||
address: 'Москва, ул. Советская, 25'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
firstName: 'Дмитрий',
|
||||
lastName: 'Сидоров',
|
||||
position: 'Водитель',
|
||||
department: 'Доставка',
|
||||
phone: '+7 (999) 345-67-89',
|
||||
email: 'd.sidorov@company.com',
|
||||
hireDate: '2022-11-10',
|
||||
status: 'vacation',
|
||||
salary: 70000,
|
||||
address: 'Москва, ул. Мира, 15'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
firstName: 'Анна',
|
||||
lastName: 'Козлова',
|
||||
position: 'HR-специалист',
|
||||
department: 'Кадры',
|
||||
phone: '+7 (999) 456-78-90',
|
||||
email: 'a.kozlova@company.com',
|
||||
hireDate: '2023-02-05',
|
||||
status: 'active',
|
||||
salary: 75000,
|
||||
address: 'Москва, пр. Победы, 8'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
interface EmployeesListProps {
|
||||
searchQuery: string
|
||||
|
@ -50,14 +50,14 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('')
|
||||
const [localSearch, setLocalSearch] = useState('')
|
||||
|
||||
const { data, loading, refetch } = useQuery(GET_ALL_PRODUCTS, {
|
||||
const { data, loading } = useQuery(GET_ALL_PRODUCTS, {
|
||||
variables: {
|
||||
search: searchTerm || null,
|
||||
category: selectedCategoryId || selectedCategory || null
|
||||
}
|
||||
})
|
||||
|
||||
const products: Product[] = data?.allProducts || []
|
||||
const products: Product[] = useMemo(() => data?.allProducts || [], [data?.allProducts])
|
||||
|
||||
// Получаем уникальные категории из товаров
|
||||
const categories = useMemo(() => {
|
||||
|
@ -5,7 +5,7 @@ import { useMutation, useQuery } from '@apollo/client'
|
||||
import { GET_MESSAGES } from '@/graphql/queries'
|
||||
import { SEND_MESSAGE, SEND_VOICE_MESSAGE, SEND_IMAGE_MESSAGE, SEND_FILE_MESSAGE } from '@/graphql/mutations'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { EmojiPickerComponent } from '@/components/ui/emoji-picker'
|
||||
|
@ -176,6 +176,7 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
|
||||
disabled={isUploading}
|
||||
className="text-white/60 hover:text-white hover:bg-white/10 h-10 w-10 p-0"
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/alt-text */}
|
||||
<Image className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
|
@ -23,7 +23,6 @@ export function VoicePlayer({ audioUrl, duration = 0, isCurrentUser = false }: V
|
||||
if (duration > 0 && (!audioDuration || audioDuration === 0)) {
|
||||
setAudioDuration(duration)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [duration, audioDuration])
|
||||
|
||||
useEffect(() => {
|
||||
@ -85,7 +84,7 @@ export function VoicePlayer({ audioUrl, duration = 0, isCurrentUser = false }: V
|
||||
audio.pause()
|
||||
}
|
||||
}
|
||||
}, [audioUrl])
|
||||
}, [audioUrl, duration])
|
||||
|
||||
const togglePlayPause = () => {
|
||||
const audio = audioRef.current
|
||||
|
@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { useMutation } from '@apollo/client'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@ -80,9 +81,11 @@ export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) {
|
||||
{/* Изображение товара */}
|
||||
<div className="relative h-48 bg-white/5 overflow-hidden">
|
||||
{product.mainImage || product.images[0] ? (
|
||||
<img
|
||||
<Image
|
||||
src={product.mainImage || product.images[0]}
|
||||
alt={product.name}
|
||||
width={300}
|
||||
height={200}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
) : (
|
||||
|
@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { useMutation, useQuery } from '@apollo/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@ -9,7 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { CREATE_PRODUCT, UPDATE_PRODUCT } from '@/graphql/mutations'
|
||||
import { GET_CATEGORIES } from '@/graphql/queries'
|
||||
import { Upload, X, Star, Plus, Image as ImageIcon } from 'lucide-react'
|
||||
import { X, Star, Upload } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface Product {
|
||||
@ -56,7 +57,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
|
||||
isActive: product?.isActive ?? true
|
||||
})
|
||||
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [isUploading] = useState(false)
|
||||
const [uploadingImages, setUploadingImages] = useState<Set<number>>(new Set())
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@ -420,9 +421,11 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={`Товар ${index + 1}`}
|
||||
width={200}
|
||||
height={150}
|
||||
className="w-full aspect-square object-cover rounded-lg"
|
||||
/>
|
||||
|
||||
|
@ -538,4 +538,19 @@ export const GET_EMPLOYEE = gql`
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_EMPLOYEE_SCHEDULE = gql`
|
||||
query GetEmployeeSchedule($employeeId: ID!, $year: Int!, $month: Int!) {
|
||||
employeeSchedule(employeeId: $employeeId, year: $year, month: $month) {
|
||||
id
|
||||
date
|
||||
status
|
||||
hoursWorked
|
||||
notes
|
||||
employee {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
@ -20,6 +20,63 @@ interface Context {
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateEmployeeInput {
|
||||
firstName: string
|
||||
lastName: string
|
||||
middleName?: string
|
||||
birthDate?: string
|
||||
avatar?: string
|
||||
passportPhoto?: string
|
||||
passportSeries?: string
|
||||
passportNumber?: string
|
||||
passportIssued?: string
|
||||
passportDate?: string
|
||||
address?: string
|
||||
position: string
|
||||
department?: string
|
||||
hireDate: string
|
||||
salary?: number
|
||||
phone: string
|
||||
email?: string
|
||||
telegram?: string
|
||||
whatsapp?: string
|
||||
emergencyContact?: string
|
||||
emergencyPhone?: string
|
||||
}
|
||||
|
||||
interface UpdateEmployeeInput {
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
middleName?: string
|
||||
birthDate?: string
|
||||
avatar?: string
|
||||
passportPhoto?: string
|
||||
passportSeries?: string
|
||||
passportNumber?: string
|
||||
passportIssued?: string
|
||||
passportDate?: string
|
||||
address?: string
|
||||
position?: string
|
||||
department?: string
|
||||
hireDate?: string
|
||||
salary?: number
|
||||
status?: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED'
|
||||
phone?: string
|
||||
email?: string
|
||||
telegram?: string
|
||||
whatsapp?: string
|
||||
emergencyContact?: string
|
||||
emergencyPhone?: string
|
||||
}
|
||||
|
||||
interface UpdateScheduleInput {
|
||||
employeeId: string
|
||||
date: string
|
||||
status: 'WORK' | 'WEEKEND' | 'VACATION' | 'SICK' | 'ABSENT'
|
||||
hoursWorked?: number
|
||||
notes?: string
|
||||
}
|
||||
|
||||
interface AuthTokenPayload {
|
||||
userId: string
|
||||
phone: string
|
||||
@ -742,6 +799,59 @@ export const resolvers = {
|
||||
})
|
||||
|
||||
return employee
|
||||
},
|
||||
|
||||
// Получить табель сотрудника за месяц
|
||||
employeeSchedule: async (_: unknown, args: { employeeId: string; year: number; month: number }, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' }
|
||||
})
|
||||
}
|
||||
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true }
|
||||
})
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError('У пользователя нет организации')
|
||||
}
|
||||
|
||||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||||
}
|
||||
|
||||
// Проверяем что сотрудник принадлежит организации
|
||||
const employee = await prisma.employee.findFirst({
|
||||
where: {
|
||||
id: args.employeeId,
|
||||
organizationId: currentUser.organization.id
|
||||
}
|
||||
})
|
||||
|
||||
if (!employee) {
|
||||
throw new GraphQLError('Сотрудник не найден')
|
||||
}
|
||||
|
||||
// Получаем записи табеля за указанный месяц
|
||||
const startDate = new Date(args.year, args.month, 1)
|
||||
const endDate = new Date(args.year, args.month + 1, 0)
|
||||
|
||||
const scheduleRecords = await prisma.employeeSchedule.findMany({
|
||||
where: {
|
||||
employeeId: args.employeeId,
|
||||
date: {
|
||||
gte: startDate,
|
||||
lte: endDate
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
date: 'asc'
|
||||
}
|
||||
})
|
||||
|
||||
return scheduleRecords
|
||||
}
|
||||
},
|
||||
|
||||
@ -3110,7 +3220,7 @@ export const resolvers = {
|
||||
},
|
||||
|
||||
// Создать сотрудника
|
||||
createEmployee: async (_: unknown, args: { input: any }, context: Context) => {
|
||||
createEmployee: async (_: unknown, args: { input: CreateEmployeeInput }, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' }
|
||||
@ -3159,7 +3269,7 @@ export const resolvers = {
|
||||
},
|
||||
|
||||
// Обновить сотрудника
|
||||
updateEmployee: async (_: unknown, args: { id: string; input: any }, context: Context) => {
|
||||
updateEmployee: async (_: unknown, args: { id: string; input: UpdateEmployeeInput }, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' }
|
||||
@ -3247,7 +3357,7 @@ export const resolvers = {
|
||||
},
|
||||
|
||||
// Обновить табель сотрудника
|
||||
updateEmployeeSchedule: async (_: unknown, args: { input: any }, context: Context) => {
|
||||
updateEmployeeSchedule: async (_: unknown, args: { input: UpdateScheduleInput }, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' }
|
||||
@ -3441,6 +3551,11 @@ export const resolvers = {
|
||||
return parent.updatedAt.toISOString()
|
||||
}
|
||||
return parent.updatedAt
|
||||
},
|
||||
employee: async (parent: { employeeId: string }) => {
|
||||
return await prisma.employee.findUnique({
|
||||
where: { id: parent.employeeId }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -47,6 +47,9 @@ export const typeDefs = gql`
|
||||
# Сотрудники организации
|
||||
myEmployees: [Employee!]!
|
||||
employee(id: ID!): Employee
|
||||
|
||||
# Табель сотрудника за месяц
|
||||
employeeSchedule(employeeId: ID!, year: Int!, month: Int!): [EmployeeSchedule!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
@ -19,7 +19,6 @@ export class S3Service {
|
||||
private static async createSignedUrl(fileName: string, fileType: string): Promise<string> {
|
||||
// Для простоты пока используем прямую загрузку через fetch
|
||||
// В продакшене лучше генерировать signed URLs на backend
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// fileType используется для будущей логики разделения по типам файлов
|
||||
const timestamp = Date.now()
|
||||
const key = `avatars/${timestamp}-${fileName}`
|
||||
|
Reference in New Issue
Block a user