Удалены неиспользуемые импорты и функции из компонентов, улучшены стили и функциональность. Обновлены компоненты для работы с изображениями, добавлены новые интерфейсы и типы данных для сотрудников. Реализована логика загрузки расписания сотрудников через GraphQL, улучшен интерфейс взаимодействия с пользователем.
This commit is contained in:
@ -155,7 +155,6 @@ export function CartItems({ cart }: CartItemsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unavailableItems = cart.items.filter(item => !item.isAvailable)
|
const unavailableItems = cart.items.filter(item => !item.isAvailable)
|
||||||
const availableItems = cart.items.filter(item => item.isAvailable)
|
|
||||||
|
|
||||||
// Группировка товаров по поставщикам
|
// Группировка товаров по поставщикам
|
||||||
const groupedItems = cart.items.reduce((groups, item) => {
|
const groupedItems = cart.items.reduce((groups, item) => {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import {
|
import {
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
@ -131,7 +131,7 @@ export function CartSummary({ cart }: CartSummaryProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.values(sellerGroups).map((group, index) => (
|
{Object.values(sellerGroups).map((group) => (
|
||||||
<div
|
<div
|
||||||
key={group.organization.id}
|
key={group.organization.id}
|
||||||
className="bg-white/5 p-3 rounded-lg text-xs"
|
className="bg-white/5 p-3 rounded-lg text-xs"
|
||||||
|
@ -18,22 +18,7 @@ export function DashboardHome() {
|
|||||||
return 'Вашей организации'
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-smooth flex">
|
<div className="min-h-screen bg-gradient-smooth flex">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"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 { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
@ -289,9 +290,11 @@ export function EmployeeEditInlineForm({ employee, onSave, onCancel, isLoading =
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{formData.passportPhoto ? (
|
{formData.passportPhoto ? (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img
|
<Image
|
||||||
src={formData.passportPhoto}
|
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"
|
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)}
|
onClick={() => setShowPassportPreview(true)}
|
||||||
/>
|
/>
|
||||||
@ -546,9 +549,11 @@ export function EmployeeEditInlineForm({ employee, onSave, onCancel, isLoading =
|
|||||||
<DialogTitle className="text-white">Фото паспорта</DialogTitle>
|
<DialogTitle className="text-white">Фото паспорта</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<img
|
<Image
|
||||||
src={formData.passportPhoto}
|
src={formData.passportPhoto}
|
||||||
alt="Паспорт"
|
alt="Паспорт"
|
||||||
|
width={600}
|
||||||
|
height={800}
|
||||||
className="max-w-full max-h-[70vh] object-contain rounded-lg"
|
className="max-w-full max-h-[70vh] object-contain rounded-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@ import { Label } from '@/components/ui/label'
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
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'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
interface Employee {
|
interface Employee {
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useRef } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import {
|
import {
|
||||||
@ -19,7 +20,7 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Calendar,
|
|
||||||
FileText,
|
FileText,
|
||||||
MessageCircle
|
MessageCircle
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@ -251,9 +252,11 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{formData.passportPhoto ? (
|
{formData.passportPhoto ? (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img
|
<Image
|
||||||
src={formData.passportPhoto}
|
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"
|
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)}
|
onClick={() => setShowPassportPreview(true)}
|
||||||
/>
|
/>
|
||||||
@ -508,9 +511,11 @@ export function EmployeeInlineForm({ onSave, onCancel, isLoading = false }: Empl
|
|||||||
<DialogTitle className="text-white">Фото паспорта</DialogTitle>
|
<DialogTitle className="text-white">Фото паспорта</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<img
|
<Image
|
||||||
src={formData.passportPhoto}
|
src={formData.passportPhoto}
|
||||||
alt="Паспорт"
|
alt="Паспорт"
|
||||||
|
width={600}
|
||||||
|
height={800}
|
||||||
className="max-w-full max-h-[70vh] object-contain rounded-lg"
|
className="max-w-full max-h-[70vh] object-contain rounded-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,13 +4,13 @@ import { useState } from 'react'
|
|||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
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 { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
User,
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
Plane,
|
Plane,
|
||||||
@ -164,17 +164,17 @@ export function EmployeeSchedule({ employees }: EmployeeScheduleProps) {
|
|||||||
const getCellStyle = (status: WorkDayStatus) => {
|
const getCellStyle = (status: WorkDayStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'work':
|
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':
|
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':
|
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':
|
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':
|
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:
|
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) => {
|
// const getStatusName = (status: WorkDayStatus) => {
|
||||||
switch (status) {
|
// switch (status) {
|
||||||
case 'work': return 'Рабочий'
|
// case 'work': return 'Рабочий'
|
||||||
case 'weekend': return 'Выходной'
|
// case 'weekend': return 'Выходной'
|
||||||
case 'vacation': return 'Отпуск'
|
// case 'vacation': return 'Отпуск'
|
||||||
case 'sick': return 'Больничный'
|
// case 'sick': return 'Больничный'
|
||||||
case 'absent': return 'Прогул'
|
// case 'absent': return 'Прогул'
|
||||||
default: return 'Неизвестно'
|
// default: return 'Неизвестно'
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Создаем массив дней для отображения
|
// Создаем массив дней для отображения
|
||||||
const calendarDays: (number | null)[] = []
|
const calendarDays: (number | null)[] = []
|
||||||
@ -224,8 +224,8 @@ export function EmployeeSchedule({ employees }: EmployeeScheduleProps) {
|
|||||||
|
|
||||||
// Получить статистику для сотрудника за месяц
|
// Получить статистику для сотрудника за месяц
|
||||||
const getMonthStats = (employeeId: string) => {
|
const getMonthStats = (employeeId: string) => {
|
||||||
const monthKey = `${currentYear}-${currentMonth}`
|
// const monthKey = `${currentYear}-${currentMonth}`
|
||||||
const schedule = schedules[`${employeeId}-${monthKey}`]
|
// const schedule = schedules[`${employeeId}-${monthKey}`]
|
||||||
|
|
||||||
let workDays = 0
|
let workDays = 0
|
||||||
let vacationDays = 0
|
let vacationDays = 0
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import { useQuery, useMutation } from '@apollo/client'
|
import { useQuery, useMutation } from '@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 { 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 { Input } from '@/components/ui/input'
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
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 { EmployeeInlineForm } from './employee-inline-form'
|
||||||
import { EmployeeEditInlineForm } from './employee-edit-inline-form'
|
import { EmployeeEditInlineForm } from './employee-edit-inline-form'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'
|
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 { CREATE_EMPLOYEE, UPDATE_EMPLOYEE, DELETE_EMPLOYEE, UPDATE_EMPLOYEE_SCHEDULE } from '@/graphql/mutations'
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
@ -32,7 +32,11 @@ import {
|
|||||||
XCircle,
|
XCircle,
|
||||||
Plane,
|
Plane,
|
||||||
Activity,
|
Activity,
|
||||||
Clock
|
Clock,
|
||||||
|
Briefcase,
|
||||||
|
MapPin,
|
||||||
|
AlertCircle,
|
||||||
|
MessageCircle
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
// Интерфейс сотрудника
|
// Интерфейс сотрудника
|
||||||
@ -70,16 +74,63 @@ export function EmployeesDashboard() {
|
|||||||
const [createLoading, setCreateLoading] = useState(false)
|
const [createLoading, setCreateLoading] = useState(false)
|
||||||
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null)
|
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null)
|
||||||
const [deletingEmployeeId, setDeletingEmployeeId] = useState<string | 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 запросы и мутации
|
// GraphQL запросы и мутации
|
||||||
const { data, loading, refetch } = useQuery(GET_MY_EMPLOYEES)
|
const { data, loading, refetch } = useQuery(GET_MY_EMPLOYEES)
|
||||||
const [createEmployee] = useMutation(CREATE_EMPLOYEE)
|
const [createEmployee] = useMutation(CREATE_EMPLOYEE)
|
||||||
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE)
|
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE)
|
||||||
const [deleteEmployee] = useMutation(DELETE_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) => {
|
const handleEditEmployee = (employee: Employee) => {
|
||||||
setEditingEmployee(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 exportToCSV = () => {
|
||||||
const csvContent = [
|
const csvContent = [
|
||||||
['ФИО', 'Должность', 'Статус', 'Зарплата', 'Телефон', 'Email', 'Дата найма'],
|
['ФИО', 'Должность', 'Статус', 'Зарплата', 'Телефон', 'Email', 'Дата найма'],
|
||||||
@ -319,10 +436,9 @@ ${employees.map((emp: Employee) =>
|
|||||||
<TabsContent value="combined">
|
<TabsContent value="combined">
|
||||||
<Card className="glass-card p-6">
|
<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.firstName} ${employee.lastName}`.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
employee.position.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
employee.position.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
(employee.department && employee.department.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (filteredEmployees.length === 0) {
|
if (filteredEmployees.length === 0) {
|
||||||
@ -376,31 +492,31 @@ ${employees.map((emp: Employee) =>
|
|||||||
{/* Легенда точно как в гите */}
|
{/* Легенда точно как в гите */}
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<CheckCircle className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white/70 text-sm">Рабочий день</span>
|
<span className="text-white/70 text-sm">Рабочий день</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<Clock className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white/70 text-sm">Выходной</span>
|
<span className="text-white/70 text-sm">Выходной</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<Plane className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white/70 text-sm">Отпуск</span>
|
<span className="text-white/70 text-sm">Отпуск</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<Activity className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white/70 text-sm">Больничный</span>
|
<span className="text-white/70 text-sm">Больничный</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<XCircle className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white/70 text-sm">Прогул</span>
|
<span className="text-white/70 text-sm">Прогул</span>
|
||||||
@ -408,7 +524,7 @@ ${employees.map((emp: Employee) =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Объединенный список сотрудников с табелем */}
|
{/* Объединенный список сотрудников с табелем */}
|
||||||
{filteredEmployees.map((employee) => {
|
{filteredEmployees.map((employee: Employee) => {
|
||||||
// Генерируем календарные дни для текущего месяца
|
// Генерируем календарные дни для текущего месяца
|
||||||
const currentDate = new Date()
|
const currentDate = new Date()
|
||||||
const currentMonth = currentDate.getMonth()
|
const currentMonth = currentDate.getMonth()
|
||||||
@ -431,16 +547,16 @@ ${employees.map((emp: Employee) =>
|
|||||||
return 'work'
|
return 'work'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDayClass = (status: string) => {
|
// const getDayClass = (status: string) => {
|
||||||
switch (status) {
|
// switch (status) {
|
||||||
case 'work': return 'bg-green-500/30 border-green-500/50'
|
// case 'work': return 'bg-green-500/30 border-green-500/50'
|
||||||
case 'weekend': return 'bg-gray-500/30 border-gray-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 'vacation': return 'bg-blue-500/30 border-blue-500/50'
|
||||||
case 'sick': return 'bg-yellow-500/30 border-yellow-500/50'
|
// case 'sick': return 'bg-yellow-500/30 border-yellow-500/50'
|
||||||
case 'absent': return 'bg-red-500/30 border-red-500/50'
|
// case 'absent': return 'bg-red-500/30 border-red-500/50'
|
||||||
default: return 'bg-white/10 border-white/20'
|
// default: return 'bg-white/10 border-white/20'
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Подсчитываем статистику
|
// Подсчитываем статистику
|
||||||
const stats = {
|
const stats = {
|
||||||
@ -471,7 +587,7 @@ ${employees.map((emp: Employee) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="flex flex-col lg:flex-row gap-6">
|
||||||
{/* Информация о сотруднике */}
|
{/* Информация о сотруднике */}
|
||||||
<div className="lg:w-80 flex-shrink-0">
|
<div className="lg:w-80 flex-shrink-0">
|
||||||
@ -489,34 +605,34 @@ ${employees.map((emp: Employee) =>
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<AvatarFallback className="bg-gradient-to-br from-purple-500 to-purple-600 text-white font-semibold text-lg">
|
<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>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h3 className="text-white font-semibold text-lg truncate">
|
<h3 className="text-white font-semibold text-lg truncate">
|
||||||
{employee.firstName} {employee.lastName}
|
{employee.firstName} {employee.lastName}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="text-white/60 hover:text-white hover:bg-white/10 h-8 w-8 p-0"
|
className="text-white/60 hover:text-white hover:bg-white/10 h-8 w-8 p-0"
|
||||||
onClick={() => handleEditEmployee(employee)}
|
onClick={() => handleEditEmployee(employee)}
|
||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="text-red-400/60 hover:text-red-300 hover:bg-red-500/10 h-8 w-8 p-0"
|
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" />
|
<UserX className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent className="glass-card border-white/10">
|
<AlertDialogContent className="glass-card border-white/10">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@ -540,52 +656,90 @@ ${employees.map((emp: Employee) =>
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Табель работы 1 в 1 как в гите */}
|
<div className="mb-3">
|
||||||
<div className="flex-1">
|
<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">
|
<h4 className="text-white/80 font-medium mb-3 flex items-center gap-2">
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
Табель работы за {new Date().toLocaleDateString('ru-RU', { month: 'long' })}
|
Табель работы за {new Date().toLocaleDateString('ru-RU', { month: 'long' })}
|
||||||
@ -597,7 +751,7 @@ ${employees.map((emp: Employee) =>
|
|||||||
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => (
|
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => (
|
||||||
<div key={day} className="p-2 text-center text-white/70 font-medium text-sm">
|
<div key={day} className="p-2 text-center text-white/70 font-medium text-sm">
|
||||||
{day}
|
{day}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Дни месяца */}
|
{/* Дни месяца */}
|
||||||
@ -617,31 +771,41 @@ ${employees.map((emp: Employee) =>
|
|||||||
calendarDays.push(day)
|
calendarDays.push(day)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функции статуса и стилей точно как в гите
|
// Функция для получения статуса дня из данных или дефолта
|
||||||
const getDayStatus = (day: number) => {
|
const getDayStatus = (day: number) => {
|
||||||
const date = new Date(currentYear, currentMonth, day)
|
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()
|
const dayOfWeek = date.getDay()
|
||||||
if (dayOfWeek === 0 || dayOfWeek === 6) return 'weekend'
|
if (dayOfWeek === 0 || dayOfWeek === 6) return 'weekend'
|
||||||
if ([15, 16].includes(day)) return 'vacation'
|
return 'work' // По умолчанию рабочий день для новых сотрудников
|
||||||
if ([10].includes(day)) return 'sick'
|
|
||||||
if ([22].includes(day)) return 'absent'
|
|
||||||
return 'work'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCellStyle = (status: string) => {
|
const getCellStyle = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'work':
|
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':
|
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':
|
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':
|
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':
|
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:
|
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}`}
|
key={`${employee.id}-${day}`}
|
||||||
className={`
|
className={`
|
||||||
relative p-2 min-h-[60px] border rounded-lg cursor-pointer
|
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)}
|
${getCellStyle(status)}
|
||||||
${isToday ? 'ring-2 ring-white/50' : ''}
|
${isToday ? 'ring-2 ring-white/50' : ''}
|
||||||
`}
|
`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Циклично переключаем статусы - точно как в гите
|
// Циклично переключаем статусы
|
||||||
// const statuses = ['work', 'weekend', 'vacation', 'sick', 'absent']
|
changeDayStatus(employee.id, day, status)
|
||||||
// const currentIndex = statuses.indexOf(status)
|
|
||||||
// const nextStatus = statuses[(currentIndex + 1) % statuses.length]
|
|
||||||
// changeDayStatus(employee.id, day, nextStatus)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
@ -698,7 +859,7 @@ ${employees.map((emp: Employee) =>
|
|||||||
{hours > 0 && (
|
{hours > 0 && (
|
||||||
<span className="text-xs opacity-80">{hours}ч</span>
|
<span className="text-xs opacity-80">{hours}ч</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isToday && (
|
{isToday && (
|
||||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-white rounded-full"></div>
|
<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>
|
{/* Статистика за месяц */}
|
||||||
|
<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>
|
</Card>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="reports">
|
<TabsContent value="reports">
|
||||||
@ -783,7 +964,7 @@ ${employees.map((emp: Employee) =>
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-white/70 text-sm">Отделов</p>
|
<p className="text-white/70 text-sm">Отделов</p>
|
||||||
<p className="text-2xl font-bold text-orange-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Calendar className="h-8 w-8 text-orange-400" />
|
<Calendar className="h-8 w-8 text-orange-400" />
|
||||||
@ -833,16 +1014,16 @@ ${employees.map((emp: Employee) =>
|
|||||||
<Card className="glass-card p-6">
|
<Card className="glass-card p-6">
|
||||||
<h3 className="text-white font-medium mb-4">Распределение по отделам</h3>
|
<h3 className="text-white font-medium mb-4">Распределение по отделам</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Array.from(new Set(employees.map(e => e.department).filter(Boolean))).map(dept => {
|
{Array.from(new Set(employees.map((e: Employee) => e.position).filter(Boolean)) as Set<string>).map((position: string) => {
|
||||||
const deptEmployees = employees.filter(e => e.department === dept)
|
const positionEmployees = employees.filter((e: Employee) => e.position === position)
|
||||||
const percentage = Math.round((deptEmployees.length / employees.length) * 100)
|
const percentage = Math.round((positionEmployees.length / employees.length) * 100)
|
||||||
|
|
||||||
return (
|
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-1">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="text-white/80 text-sm">{dept}</span>
|
<span className="text-white/80 text-sm">{position}</span>
|
||||||
<span className="text-white/60 text-xs">{deptEmployees.length} чел. ({percentage}%)</span>
|
<span className="text-white/60 text-xs">{positionEmployees.length} чел. ({percentage}%)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-white/10 rounded-full h-2">
|
<div className="w-full bg-white/10 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
|
@ -5,7 +5,7 @@ import { Card } from '@/components/ui/card'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||||
import { Badge } from '@/components/ui/badge'
|
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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
@ -20,7 +20,7 @@ import {
|
|||||||
Briefcase,
|
Briefcase,
|
||||||
Save,
|
Save,
|
||||||
X,
|
X,
|
||||||
Trash2,
|
|
||||||
UserX
|
UserX
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
@ -40,61 +40,7 @@ interface Employee {
|
|||||||
address: string
|
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 {
|
interface EmployeesListProps {
|
||||||
searchQuery: string
|
searchQuery: string
|
||||||
|
@ -50,14 +50,14 @@ export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBac
|
|||||||
const [selectedCategory, setSelectedCategory] = useState<string>('')
|
const [selectedCategory, setSelectedCategory] = useState<string>('')
|
||||||
const [localSearch, setLocalSearch] = useState('')
|
const [localSearch, setLocalSearch] = useState('')
|
||||||
|
|
||||||
const { data, loading, refetch } = useQuery(GET_ALL_PRODUCTS, {
|
const { data, loading } = useQuery(GET_ALL_PRODUCTS, {
|
||||||
variables: {
|
variables: {
|
||||||
search: searchTerm || null,
|
search: searchTerm || null,
|
||||||
category: selectedCategoryId || selectedCategory || null
|
category: selectedCategoryId || selectedCategory || null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const products: Product[] = data?.allProducts || []
|
const products: Product[] = useMemo(() => data?.allProducts || [], [data?.allProducts])
|
||||||
|
|
||||||
// Получаем уникальные категории из товаров
|
// Получаем уникальные категории из товаров
|
||||||
const categories = useMemo(() => {
|
const categories = useMemo(() => {
|
||||||
|
@ -5,7 +5,7 @@ import { useMutation, useQuery } from '@apollo/client'
|
|||||||
import { GET_MESSAGES } from '@/graphql/queries'
|
import { GET_MESSAGES } from '@/graphql/queries'
|
||||||
import { SEND_MESSAGE, SEND_VOICE_MESSAGE, SEND_IMAGE_MESSAGE, SEND_FILE_MESSAGE } from '@/graphql/mutations'
|
import { SEND_MESSAGE, SEND_VOICE_MESSAGE, SEND_IMAGE_MESSAGE, SEND_FILE_MESSAGE } from '@/graphql/mutations'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { EmojiPickerComponent } from '@/components/ui/emoji-picker'
|
import { EmojiPickerComponent } from '@/components/ui/emoji-picker'
|
||||||
|
@ -176,6 +176,7 @@ export function FileUploader({ onSendFile }: FileUploaderProps) {
|
|||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
className="text-white/60 hover:text-white hover:bg-white/10 h-10 w-10 p-0"
|
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" />
|
<Image className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ export function VoicePlayer({ audioUrl, duration = 0, isCurrentUser = false }: V
|
|||||||
if (duration > 0 && (!audioDuration || audioDuration === 0)) {
|
if (duration > 0 && (!audioDuration || audioDuration === 0)) {
|
||||||
setAudioDuration(duration)
|
setAudioDuration(duration)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [duration, audioDuration])
|
}, [duration, audioDuration])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -85,7 +84,7 @@ export function VoicePlayer({ audioUrl, duration = 0, isCurrentUser = false }: V
|
|||||||
audio.pause()
|
audio.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [audioUrl])
|
}, [audioUrl, duration])
|
||||||
|
|
||||||
const togglePlayPause = () => {
|
const togglePlayPause = () => {
|
||||||
const audio = audioRef.current
|
const audio = audioRef.current
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from 'react'
|
import Image from 'next/image'
|
||||||
|
|
||||||
import { useMutation } from '@apollo/client'
|
import { useMutation } from '@apollo/client'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
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">
|
<div className="relative h-48 bg-white/5 overflow-hidden">
|
||||||
{product.mainImage || product.images[0] ? (
|
{product.mainImage || product.images[0] ? (
|
||||||
<img
|
<Image
|
||||||
src={product.mainImage || product.images[0]}
|
src={product.mainImage || product.images[0]}
|
||||||
alt={product.name}
|
alt={product.name}
|
||||||
|
width={300}
|
||||||
|
height={200}
|
||||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
|
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useRef } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
import { useMutation, useQuery } from '@apollo/client'
|
import { useMutation, useQuery } from '@apollo/client'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
@ -9,7 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { CREATE_PRODUCT, UPDATE_PRODUCT } from '@/graphql/mutations'
|
import { CREATE_PRODUCT, UPDATE_PRODUCT } from '@/graphql/mutations'
|
||||||
import { GET_CATEGORIES } from '@/graphql/queries'
|
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'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
interface Product {
|
interface Product {
|
||||||
@ -56,7 +57,7 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
|
|||||||
isActive: product?.isActive ?? true
|
isActive: product?.isActive ?? true
|
||||||
})
|
})
|
||||||
|
|
||||||
const [isUploading, setIsUploading] = useState(false)
|
const [isUploading] = useState(false)
|
||||||
const [uploadingImages, setUploadingImages] = useState<Set<number>>(new Set())
|
const [uploadingImages, setUploadingImages] = useState<Set<number>>(new Set())
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
@ -420,9 +421,11 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<img
|
<Image
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={`Товар ${index + 1}`}
|
alt={`Товар ${index + 1}`}
|
||||||
|
width={200}
|
||||||
|
height={150}
|
||||||
className="w-full aspect-square object-cover rounded-lg"
|
className="w-full aspect-square object-cover rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -539,3 +539,18 @@ export const GET_EMPLOYEE = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
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 {
|
interface AuthTokenPayload {
|
||||||
userId: string
|
userId: string
|
||||||
phone: string
|
phone: string
|
||||||
@ -742,6 +799,59 @@ export const resolvers = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return employee
|
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) {
|
if (!context.user) {
|
||||||
throw new GraphQLError('Требуется авторизация', {
|
throw new GraphQLError('Требуется авторизация', {
|
||||||
extensions: { code: 'UNAUTHENTICATED' }
|
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) {
|
if (!context.user) {
|
||||||
throw new GraphQLError('Требуется авторизация', {
|
throw new GraphQLError('Требуется авторизация', {
|
||||||
extensions: { code: 'UNAUTHENTICATED' }
|
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) {
|
if (!context.user) {
|
||||||
throw new GraphQLError('Требуется авторизация', {
|
throw new GraphQLError('Требуется авторизация', {
|
||||||
extensions: { code: 'UNAUTHENTICATED' }
|
extensions: { code: 'UNAUTHENTICATED' }
|
||||||
@ -3441,6 +3551,11 @@ export const resolvers = {
|
|||||||
return parent.updatedAt.toISOString()
|
return parent.updatedAt.toISOString()
|
||||||
}
|
}
|
||||||
return parent.updatedAt
|
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!]!
|
myEmployees: [Employee!]!
|
||||||
employee(id: ID!): Employee
|
employee(id: ID!): Employee
|
||||||
|
|
||||||
|
# Табель сотрудника за месяц
|
||||||
|
employeeSchedule(employeeId: ID!, year: Int!, month: Int!): [EmployeeSchedule!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
@ -19,7 +19,6 @@ export class S3Service {
|
|||||||
private static async createSignedUrl(fileName: string, fileType: string): Promise<string> {
|
private static async createSignedUrl(fileName: string, fileType: string): Promise<string> {
|
||||||
// Для простоты пока используем прямую загрузку через fetch
|
// Для простоты пока используем прямую загрузку через fetch
|
||||||
// В продакшене лучше генерировать signed URLs на backend
|
// В продакшене лучше генерировать signed URLs на backend
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
// fileType используется для будущей логики разделения по типам файлов
|
// fileType используется для будущей логики разделения по типам файлов
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const key = `avatars/${timestamp}-${fileName}`
|
const key = `avatars/${timestamp}-${fileName}`
|
||||||
|
Reference in New Issue
Block a user