Исправлены критические ошибки типизации и React Hooks

• Исправлена ошибка React Hooks в EmployeesDashboard - перемещен useMemo на верхний уровень компонента
• Устранены ошибки TypeScript в ScheduleRecord интерфейсе
• Добавлена типизация GraphQL скаляров и резолверов
• Исправлены типы Apollo Client и error handling
• Очищены неиспользуемые импорты в компонентах Employee
• Переименованы неиспользуемые переменные в warehouse-statistics
• Исправлен экспорт RefreshCw иконки

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-06 14:25:30 +03:00
parent 940aa0b834
commit c2b342a527
9 changed files with 251 additions and 251 deletions

View File

@ -1,21 +1,6 @@
'use client' 'use client'
import { import { User, UserPlus, AlertCircle, Save, X, Camera, RefreshCw } from 'lucide-react'
User,
UserPlus,
Phone,
Mail,
Briefcase,
DollarSign,
AlertCircle,
Save,
X,
Camera,
Calendar,
MessageCircle,
FileImage,
RefreshCw,
} from 'lucide-react'
import { useState, useRef } from 'react' import { useState, useRef } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'

View File

@ -6,16 +6,15 @@ import {
X, X,
Save, Save,
UserPlus, UserPlus,
Phone,
Mail,
Briefcase,
DollarSign,
FileText,
MessageCircle,
AlertCircle, AlertCircle,
Calendar,
RefreshCw, RefreshCw,
FileImage, FileImage,
Briefcase,
Phone,
Mail,
Calendar,
DollarSign,
MessageCircle,
} from 'lucide-react' } from 'lucide-react'
import Image from 'next/image' import Image from 'next/image'
import { useState, useRef } from 'react' import { useState, useRef } from 'react'
@ -23,23 +22,16 @@ import { toast } from 'sonner'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { import {
formatPhoneInput, formatPhoneInput,
formatPassportSeries,
formatPassportNumber,
formatSalary, formatSalary,
formatNameInput, formatNameInput,
isValidEmail, isValidEmail,
isValidPhone, isValidPhone,
isValidPassportSeries,
isValidPassportNumber,
isValidBirthDate, isValidBirthDate,
isValidHireDate,
isValidSalary, isValidSalary,
} from '@/lib/input-masks' } from '@/lib/input-masks'

View File

@ -97,6 +97,17 @@ const EmployeesDashboard = React.memo(() => {
const employees = useMemo(() => data?.myEmployees || [], [data?.myEmployees]) const employees = useMemo(() => data?.myEmployees || [], [data?.myEmployees])
// Фильтрация сотрудников на верхнем уровне компонента (исправление Rules of Hooks)
const filteredEmployees = useMemo(
() =>
employees.filter(
(employee: Employee) =>
`${employee.firstName} ${employee.lastName}`.toLowerCase().includes(searchQuery.toLowerCase()) ||
employee.position.toLowerCase().includes(searchQuery.toLowerCase()),
),
[employees, searchQuery],
)
// Загружаем данные табеля для всех сотрудников // Загружаем данные табеля для всех сотрудников
useEffect(() => { useEffect(() => {
const loadScheduleData = async () => { const loadScheduleData = async () => {
@ -141,140 +152,156 @@ const EmployeesDashboard = React.memo(() => {
setShowAddForm(false) // Закрываем форму добавления если открыта setShowAddForm(false) // Закрываем форму добавления если открыта
}, []) }, [])
const handleEmployeeSaved = useCallback(async (employeeData: Partial<Employee>) => { const handleEmployeeSaved = useCallback(
try { async (employeeData: Partial<Employee>) => {
if (editingEmployee) { try {
// Обновление существующего сотрудника if (editingEmployee) {
const { data } = await updateEmployee({ // Обновление существующего сотрудника
variables: { const { data } = await updateEmployee({
id: editingEmployee.id, variables: {
input: employeeData, id: editingEmployee.id,
}, input: employeeData,
}) },
if (data?.updateEmployee?.success) { })
toast.success('Сотрудник успешно обновлен') if (data?.updateEmployee?.success) {
refetch() toast.success('Сотрудник успешно обновлен')
refetch()
}
} else {
// Добавление нового сотрудника
const { data } = await createEmployee({
variables: { input: employeeData },
})
if (data?.createEmployee?.success) {
toast.success('Сотрудник успешно добавлен')
refetch()
}
} }
} else { setShowEditForm(false)
// Добавление нового сотрудника setEditingEmployee(null)
} catch (error) {
console.error('Error saving employee:', error)
toast.error('Ошибка при сохранении сотрудника')
}
},
[editingEmployee, updateEmployee, createEmployee, refetch],
)
const handleCreateEmployee = useCallback(
async (employeeData: Partial<Employee>) => {
setCreateLoading(true)
try {
const { data } = await createEmployee({ const { data } = await createEmployee({
variables: { input: employeeData }, variables: { input: employeeData },
}) })
if (data?.createEmployee?.success) { if (data?.createEmployee?.success) {
toast.success('Сотрудник успешно добавлен') toast.success('Сотрудник успешно добавлен!')
setShowAddForm(false)
refetch() refetch()
} }
} catch (error) {
console.error('Error creating employee:', error)
toast.error('Ошибка при создании сотрудника')
} finally {
setCreateLoading(false)
} }
setShowEditForm(false) },
setEditingEmployee(null) [createEmployee, refetch],
} catch (error) { )
console.error('Error saving employee:', error)
toast.error('Ошибка при сохранении сотрудника')
}
}, [editingEmployee, updateEmployee, createEmployee, refetch])
const handleCreateEmployee = useCallback(async (employeeData: Partial<Employee>) => { const handleEmployeeDeleted = useCallback(
setCreateLoading(true) async (employeeId: string) => {
try { try {
const { data } = await createEmployee({ setDeletingEmployeeId(employeeId)
variables: { input: employeeData }, const { data } = await deleteEmployee({
}) variables: { id: employeeId },
if (data?.createEmployee?.success) { })
toast.success('Сотрудник успешно добавлен!') if (data?.deleteEmployee) {
setShowAddForm(false) toast.success('Сотрудник успешно уволен')
refetch() refetch()
}
} catch (error) {
console.error('Error deleting employee:', error)
toast.error('Ошибка при увольнении сотрудника')
} finally {
setDeletingEmployeeId(null)
} }
} catch (error) { },
console.error('Error creating employee:', error) [deleteEmployee, refetch],
toast.error('Ошибка при создании сотрудника') )
} finally {
setCreateLoading(false)
}
}, [createEmployee, refetch])
const handleEmployeeDeleted = useCallback(async (employeeId: string) => {
try {
setDeletingEmployeeId(employeeId)
const { data } = await deleteEmployee({
variables: { id: employeeId },
})
if (data?.deleteEmployee) {
toast.success('Сотрудник успешно уволен')
refetch()
}
} catch (error) {
console.error('Error deleting employee:', error)
toast.error('Ошибка при увольнении сотрудника')
} finally {
setDeletingEmployeeId(null)
}
}, [deleteEmployee, refetch])
// Функция для изменения статуса дня в табеле // Функция для изменения статуса дня в табеле
const changeDayStatus = useCallback(async (employeeId: string, day: number, currentStatus: string) => { const changeDayStatus = useCallback(
try { async (employeeId: string, day: number, currentStatus: string) => {
// Циклично переключаем статусы try {
const statuses = ['WORK', 'WEEKEND', 'VACATION', 'SICK', 'ABSENT'] // Циклично переключаем статусы
const currentIndex = statuses.indexOf(currentStatus.toUpperCase()) const statuses = ['WORK', 'WEEKEND', 'VACATION', 'SICK', 'ABSENT']
const nextStatus = statuses[(currentIndex + 1) % statuses.length] const currentIndex = statuses.indexOf(currentStatus.toUpperCase())
const nextStatus = statuses[(currentIndex + 1) % statuses.length]
// Формируем дату // Формируем дату
const date = new Date(currentYear, currentMonth, day) const date = new Date(currentYear, currentMonth, day)
const hours = nextStatus === 'WORK' ? 8 : 0 const hours = nextStatus === 'WORK' ? 8 : 0
// Отправляем мутацию // Отправляем мутацию
await updateEmployeeSchedule({ await updateEmployeeSchedule({
variables: { variables: {
input: { input: {
employeeId: employeeId, employeeId: employeeId,
date: date.toISOString().split('T')[0], // YYYY-MM-DD формат 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: ScheduleRecord = {
id: Date.now().toString(), // временный ID
date: updatedDate.toISOString(),
status: nextStatus, status: nextStatus,
hoursWorked: hours, hoursWorked: hours,
}, employee: {
}, id: employeeId,
}) firstName: '',
lastName: '',
// Обновляем локальное состояние },
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 { let updatedSchedule
...prev, if (existingRecordIndex >= 0) {
[employeeId]: updatedSchedule, // Обновляем существующую запись
} updatedSchedule = [...currentSchedule]
}) updatedSchedule[existingRecordIndex] = {
...updatedSchedule[existingRecordIndex],
...newRecord,
}
} else {
// Добавляем новую запись
updatedSchedule = [...currentSchedule, newRecord]
}
toast.success('Статус дня обновлен') return {
} catch (error) { ...prev,
console.error('Error updating day status:', error) [employeeId]: updatedSchedule,
toast.error('Ошибка при обновлении статуса дня') }
} })
}, [updateEmployeeSchedule, currentYear, currentMonth, setEmployeeSchedules])
toast.success('Статус дня обновлен')
} catch (error) {
console.error('Error updating day status:', error)
toast.error('Ошибка при обновлении статуса дня')
}
},
[updateEmployeeSchedule, currentYear, currentMonth, setEmployeeSchedules],
)
// Функция для обновления данных дня из модалки // Функция для обновления данных дня из модалки
const updateDayData = async ( const updateDayData = async (
@ -309,14 +336,18 @@ const EmployeesDashboard = React.memo(() => {
const currentSchedule = prev[employeeId] || [] const currentSchedule = prev[employeeId] || []
const existingRecordIndex = currentSchedule.findIndex((record) => record.date.split('T')[0] === dateStr) const existingRecordIndex = currentSchedule.findIndex((record) => record.date.split('T')[0] === dateStr)
const newRecord = { const newRecord: ScheduleRecord = {
id: Date.now().toString(), // временный ID id: Date.now().toString(), // временный ID
date: date.toISOString(), date: date.toISOString(),
status: data.status, status: data.status,
hoursWorked: data.hoursWorked, hoursWorked: data.hoursWorked,
overtimeHours: data.overtimeHours, overtimeHours: data.overtimeHours,
notes: data.notes, notes: data.notes,
employee: { id: employeeId }, employee: {
id: employeeId,
firstName: '',
lastName: '',
},
} }
let updatedSchedule let updatedSchedule
@ -529,97 +560,87 @@ ${employees.map((emp: Employee) => `• ${emp.firstName} ${emp.lastName} - ${emp
{/* Контент табов */} {/* Контент табов */}
<TabsContent value="combined"> <TabsContent value="combined">
<Card className="glass-card p-6"> <Card className="glass-card p-6">
{(() => { {filteredEmployees.length === 0 ? (
const filteredEmployees = useMemo(() => employees.filter( <EmployeeEmptyState searchQuery={searchQuery} onShowAddForm={() => setShowAddForm(true)} />
(employee: Employee) => ) : (
`${employee.firstName} ${employee.lastName}`.toLowerCase().includes(searchQuery.toLowerCase()) || <div className="space-y-6">
employee.position.toLowerCase().includes(searchQuery.toLowerCase()), {/* Навигация по месяцам и легенда в одной строке */}
), [employees, searchQuery]) <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<h3 className="text-white font-medium text-lg capitalize">
{new Date().toLocaleDateString('ru-RU', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</h3>
if (filteredEmployees.length === 0) { {/* Кнопки навигации */}
return <EmployeeEmptyState searchQuery={searchQuery} onShowAddForm={() => setShowAddForm(true)} /> <div className="flex gap-2">
} <Button
variant="outline"
return ( size="sm"
<div className="space-y-6"> className="glass-secondary text-white hover:text-white h-8 px-3"
{/* Навигация по месяцам и легенда в одной строке */} onClick={() => {
<div className="flex items-center justify-between mb-6"> const newDate = new Date(currentYear, currentMonth - 1)
<div className="flex items-center gap-4"> setCurrentYear(newDate.getFullYear())
<h3 className="text-white font-medium text-lg capitalize"> setCurrentMonth(newDate.getMonth())
{new Date().toLocaleDateString('ru-RU', { }}
weekday: 'long', >
day: 'numeric',
month: 'long', </Button>
year: 'numeric', <Button
})} variant="outline"
</h3> size="sm"
className="glass-secondary text-white hover:text-white h-8 px-3"
{/* Кнопки навигации */} onClick={() => {
<div className="flex gap-2"> const today = new Date()
<Button setCurrentYear(today.getFullYear())
variant="outline" setCurrentMonth(today.getMonth())
size="sm" }}
className="glass-secondary text-white hover:text-white h-8 px-3" >
onClick={() => { Сегодня
const newDate = new Date(currentYear, currentMonth - 1) </Button>
setCurrentYear(newDate.getFullYear()) <Button
setCurrentMonth(newDate.getMonth()) variant="outline"
}} size="sm"
> className="glass-secondary text-white hover:text-white h-8 px-3"
onClick={() => {
</Button> const newDate = new Date(currentYear, currentMonth + 1)
<Button setCurrentYear(newDate.getFullYear())
variant="outline" setCurrentMonth(newDate.getMonth())
size="sm" }}
className="glass-secondary text-white hover:text-white h-8 px-3" >
onClick={() => {
const today = new Date() </Button>
setCurrentYear(today.getFullYear())
setCurrentMonth(today.getMonth())
}}
>
Сегодня
</Button>
<Button
variant="outline"
size="sm"
className="glass-secondary text-white hover:text-white h-8 px-3"
onClick={() => {
const newDate = new Date(currentYear, currentMonth + 1)
setCurrentYear(newDate.getFullYear())
setCurrentMonth(newDate.getMonth())
}}
>
</Button>
</div>
</div> </div>
{/* Легенда статусов справа */}
<EmployeeLegend />
</div> </div>
{/* Компактный список сотрудников с раскрывающимся табелем */} {/* Легенда статусов справа */}
<div> <EmployeeLegend />
{filteredEmployees.map((employee: Employee, index: number) => (
<div key={employee.id} className={index < filteredEmployees.length - 1 ? 'mb-4' : ''}>
<EmployeeRow
employee={employee}
employeeSchedules={employeeSchedules}
currentYear={currentYear}
currentMonth={currentMonth}
onEdit={handleEditEmployee}
onDelete={handleEmployeeDeleted}
onDayStatusChange={changeDayStatus}
onDayUpdate={updateDayData}
deletingEmployeeId={deletingEmployeeId}
/>
</div>
))}
</div>
</div> </div>
)
})()} {/* Компактный список сотрудников с раскрывающимся табелем */}
<div>
{filteredEmployees.map((employee: Employee, index: number) => (
<div key={employee.id} className={index < filteredEmployees.length - 1 ? 'mb-4' : ''}>
<EmployeeRow
employee={employee}
employeeSchedules={employeeSchedules}
currentYear={currentYear}
currentMonth={currentMonth}
onEdit={handleEditEmployee}
onDelete={handleEmployeeDeleted}
onDayStatusChange={changeDayStatus}
onDayUpdate={updateDayData}
deletingEmployeeId={deletingEmployeeId}
/>
</div>
))}
</div>
</div>
)}
</Card> </Card>
</TabsContent> </TabsContent>

View File

@ -11,7 +11,7 @@ export {
Download, Download,
Search, Search,
Filter, Filter,
Refresh as RefreshCw, RefreshCw,
// Навигация // Навигация
ArrowLeft, ArrowLeft,

View File

@ -36,18 +36,18 @@ export function WarehouseStatistics({ products }: WarehouseStatisticsProps) {
const totalStock = products.reduce((sum, p) => sum + (p.stock || p.quantity || 0), 0) const totalStock = products.reduce((sum, p) => sum + (p.stock || p.quantity || 0), 0)
const totalOrdered = products.reduce((sum, p) => sum + (p.ordered || 0), 0) const totalOrdered = products.reduce((sum, p) => sum + (p.ordered || 0), 0)
const totalInTransit = products.reduce((sum, p) => sum + (p.inTransit || 0), 0) const totalInTransit = products.reduce((sum, p) => sum + (p.inTransit || 0), 0)
const totalSold = products.reduce((sum, p) => sum + (p.sold || 0), 0) const _totalSold = products.reduce((sum, p) => sum + (p.sold || 0), 0)
// Статистика по товарам // Статистика по товарам
const goodsStock = goods.reduce((sum, p) => sum + (p.stock || p.quantity || 0), 0) const goodsStock = goods.reduce((sum, p) => sum + (p.stock || p.quantity || 0), 0)
const goodsOrdered = goods.reduce((sum, p) => sum + (p.ordered || 0), 0) const _goodsOrdered = goods.reduce((sum, p) => sum + (p.ordered || 0), 0)
const goodsInTransit = goods.reduce((sum, p) => sum + (p.inTransit || 0), 0) const _goodsInTransit = goods.reduce((sum, p) => sum + (p.inTransit || 0), 0)
const goodsSold = goods.reduce((sum, p) => sum + (p.sold || 0), 0) const goodsSold = goods.reduce((sum, p) => sum + (p.sold || 0), 0)
// Статистика по расходникам // Статистика по расходникам
const consumablesStock = consumables.reduce((sum, p) => sum + (p.stock || p.quantity || 0), 0) const consumablesStock = consumables.reduce((sum, p) => sum + (p.stock || p.quantity || 0), 0)
const consumablesOrdered = consumables.reduce((sum, p) => sum + (p.ordered || 0), 0) const _consumablesOrdered = consumables.reduce((sum, p) => sum + (p.ordered || 0), 0)
const consumablesInTransit = consumables.reduce((sum, p) => sum + (p.inTransit || 0), 0) const _consumablesInTransit = consumables.reduce((sum, p) => sum + (p.inTransit || 0), 0)
const consumablesSold = consumables.reduce((sum, p) => sum + (p.sold || 0), 0) const consumablesSold = consumables.reduce((sum, p) => sum + (p.sold || 0), 0)
// Товары с низкими остатками // Товары с низкими остатками

View File

@ -21,10 +21,10 @@ const mergeResolvers = (...resolvers: ResolverObject[]): ResolverObject => {
} }
for (const resolver of resolvers) { for (const resolver of resolvers) {
if (resolver.Query) { if (resolver?.Query) {
Object.assign(result.Query, resolver.Query) Object.assign(result.Query, resolver.Query)
} }
if (resolver.Mutation) { if (resolver?.Mutation) {
Object.assign(result.Mutation, resolver.Mutation) Object.assign(result.Mutation, resolver.Mutation)
} }
// Объединяем другие типы резолверов (например, Employee, Organization и т.д.) // Объединяем другие типы резолверов (например, Employee, Organization и т.д.)
@ -33,7 +33,9 @@ const mergeResolvers = (...resolvers: ResolverObject[]): ResolverObject => {
if (!result[key]) { if (!result[key]) {
result[key] = {} result[key] = {}
} }
Object.assign(result[key], value) if (typeof value === 'object' && value !== null) {
Object.assign(result[key], value)
}
} }
} }
} }

View File

@ -1,10 +1,10 @@
import { GraphQLScalarType, Kind } from 'graphql' import { GraphQLScalarType, Kind, ValueNode } from 'graphql'
export const JSONScalar = new GraphQLScalarType({ export const JSONScalar: GraphQLScalarType = new GraphQLScalarType({
name: 'JSON', name: 'JSON',
serialize: (value) => value, serialize: (value: any): any => value,
parseValue: (value) => value, parseValue: (value: any): any => value,
parseLiteral: (ast) => { parseLiteral: (ast: ValueNode): any => {
switch (ast.kind) { switch (ast.kind) {
case Kind.STRING: case Kind.STRING:
case Kind.BOOLEAN: case Kind.BOOLEAN:

View File

@ -49,7 +49,7 @@ const errorLink = onError(({ graphQLErrors, networkError, operation, forward: _f
graphQLErrorsLength: graphQLErrors?.length || 0, graphQLErrorsLength: graphQLErrors?.length || 0,
hasNetworkError: !!networkError, hasNetworkError: !!networkError,
operationName: operation?.operationName || 'Unknown', operationName: operation?.operationName || 'Unknown',
operationType: operation?.query?.definitions?.[0]?.operation || 'Unknown', operationType: (operation?.query?.definitions?.[0] as any)?.operation || 'Unknown',
variables: operation?.variables || {}, variables: operation?.variables || {},
} }
@ -85,7 +85,7 @@ const errorLink = onError(({ graphQLErrors, networkError, operation, forward: _f
try { try {
console.warn('🌐 Network Error:', { console.warn('🌐 Network Error:', {
message: networkError.message || 'No message', message: networkError.message || 'No message',
statusCode: networkError.statusCode || 'No status', statusCode: (networkError as any).statusCode || 'No status',
operation: operation?.operationName || 'Unknown', operation: operation?.operationName || 'Unknown',
}) })
} catch (innerError) { } catch (innerError) {

View File

@ -89,7 +89,7 @@ export async function ensureCategories() {
}) })
createdCount++ createdCount++
} }
} catch (error) { } catch (error: any) {
// Игнорируем ошибки дублирования // Игнорируем ошибки дублирования
if (error.code !== 'P2002') { if (error.code !== 'P2002') {
console.error(`Ошибка создания категории "${categoryName}":`, error.message) console.error(`Ошибка создания категории "${categoryName}":`, error.message)