('')
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(() => {
diff --git a/src/components/messenger/messenger-chat.tsx b/src/components/messenger/messenger-chat.tsx
index d1fd178..e312a27 100644
--- a/src/components/messenger/messenger-chat.tsx
+++ b/src/components/messenger/messenger-chat.tsx
@@ -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'
diff --git a/src/components/ui/file-uploader.tsx b/src/components/ui/file-uploader.tsx
index 503bc0b..f1cc588 100644
--- a/src/components/ui/file-uploader.tsx
+++ b/src/components/ui/file-uploader.tsx
@@ -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 */}
diff --git a/src/components/ui/voice-player.tsx b/src/components/ui/voice-player.tsx
index 388f915..01c7fa1 100644
--- a/src/components/ui/voice-player.tsx
+++ b/src/components/ui/voice-player.tsx
@@ -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
diff --git a/src/components/warehouse/product-card.tsx b/src/components/warehouse/product-card.tsx
index 7dbc54e..f27d5a4 100644
--- a/src/components/warehouse/product-card.tsx
+++ b/src/components/warehouse/product-card.tsx
@@ -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) {
{/* Изображение товара */}
{product.mainImage || product.images[0] ? (
-

) : (
diff --git a/src/components/warehouse/product-form.tsx b/src/components/warehouse/product-form.tsx
index 9e33a95..66168ef 100644
--- a/src/components/warehouse/product-form.tsx
+++ b/src/components/warehouse/product-form.tsx
@@ -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
>(new Set())
const fileInputRef = useRef(null)
@@ -420,9 +421,11 @@ export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
) : (
<>
-

diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts
index ed22412..9458fb0 100644
--- a/src/graphql/queries.ts
+++ b/src/graphql/queries.ts
@@ -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
+ }
+ }
+ }
`
\ No newline at end of file
diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts
index 33a4913..eb73697 100644
--- a/src/graphql/resolvers.ts
+++ b/src/graphql/resolvers.ts
@@ -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 }
+ })
}
}
}
\ No newline at end of file
diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts
index ab926dd..1f4acbe 100644
--- a/src/graphql/typedefs.ts
+++ b/src/graphql/typedefs.ts
@@ -47,6 +47,9 @@ export const typeDefs = gql`
# Сотрудники организации
myEmployees: [Employee!]!
employee(id: ID!): Employee
+
+ # Табель сотрудника за месяц
+ employeeSchedule(employeeId: ID!, year: Int!, month: Int!): [EmployeeSchedule!]!
}
type Mutation {
diff --git a/src/services/s3-service.ts b/src/services/s3-service.ts
index e5cabf6..26f8ef3 100644
--- a/src/services/s3-service.ts
+++ b/src/services/s3-service.ts
@@ -19,7 +19,6 @@ export class S3Service {
private static async createSignedUrl(fileName: string, fileType: string): Promise
{
// Для простоты пока используем прямую загрузку через fetch
// В продакшене лучше генерировать signed URLs на backend
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
// fileType используется для будущей логики разделения по типам файлов
const timestamp = Date.now()
const key = `avatars/${timestamp}-${fileName}`