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

This commit is contained in:
Bivekich
2025-07-18 15:40:12 +03:00
parent 7e7e4a9b4a
commit 93bb5827d2
20 changed files with 5015 additions and 667 deletions

View File

@ -1,13 +1,11 @@
"use client"
import { useState } from 'react'
import { useState, useEffect, useMemo } from 'react'
import { useQuery, useMutation } from '@apollo/client'
import { Card } from '@/components/ui/card'
import Image from 'next/image'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import {
AlertDialog,
AlertDialogAction,
@ -19,7 +17,7 @@ import {
AlertDialogTitle,
AlertDialogTrigger
} from '@/components/ui/alert-dialog'
import { Plus, Edit, Trash2, Upload, Package } from 'lucide-react'
import { Plus, Trash2, Save, X, Edit, Upload, Check } from 'lucide-react'
import { toast } from "sonner"
import { useAuth } from '@/hooks/useAuth'
import { GET_MY_SUPPLIES } from '@/graphql/queries'
@ -30,25 +28,33 @@ interface Supply {
name: string
description?: string
price: number
quantity: number
imageUrl?: string
createdAt: string
updatedAt: string
}
interface EditableSupply {
id?: string // undefined для новых записей
name: string
description: string
price: string
imageUrl: string
imageFile?: File
isNew: boolean
isEditing: boolean
hasChanges: boolean
}
interface PendingChange extends EditableSupply {
isDeleted?: boolean
}
export function SuppliesTab() {
const { user } = useAuth()
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [editingSupply, setEditingSupply] = useState<Supply | null>(null)
const [formData, setFormData] = useState({
name: '',
description: '',
price: '',
quantity: '',
imageUrl: ''
})
const [imageFile, setImageFile] = useState<File | null>(null)
const [isUploading] = useState(false)
const [editableSupplies, setEditableSupplies] = useState<EditableSupply[]>([])
const [pendingChanges, setPendingChanges] = useState<PendingChange[]>([])
const [isSaving, setIsSaving] = useState(false)
const [isInitialized, setIsInitialized] = useState(false)
// GraphQL запросы и мутации
const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, {
@ -60,45 +66,139 @@ export function SuppliesTab() {
const supplies = data?.mySupplies || []
const resetForm = () => {
setFormData({
// Преобразуем загруженные расходники в редактируемый формат
useEffect(() => {
if (data?.mySupplies && !isInitialized) {
const convertedSupplies: EditableSupply[] = data.mySupplies.map((supply: Supply) => ({
id: supply.id,
name: supply.name,
description: supply.description || '',
price: supply.price.toString(),
imageUrl: supply.imageUrl || '',
isNew: false,
isEditing: false,
hasChanges: false
}))
setEditableSupplies(convertedSupplies)
setPendingChanges([])
setIsInitialized(true)
}
}, [data, isInitialized])
// Добавить новую строку
const addNewRow = () => {
const tempId = `temp-${Date.now()}-${Math.random()}`
const newRow: EditableSupply = {
id: tempId,
name: '',
description: '',
price: '',
quantity: '',
imageUrl: ''
})
setImageFile(null)
setEditingSupply(null)
imageUrl: '',
isNew: true,
isEditing: true,
hasChanges: false
}
setEditableSupplies(prev => [...prev, newRow])
}
const handleEdit = (supply: Supply) => {
setEditingSupply(supply)
setFormData({
name: supply.name,
description: supply.description || '',
price: supply.price.toString(),
quantity: supply.quantity.toString(),
imageUrl: supply.imageUrl || ''
})
setIsDialogOpen(true)
}
const handleDelete = async (supplyId: string) => {
try {
await deleteSupply({
variables: { id: supplyId }
})
await refetch()
toast.success('Расходник успешно удален')
} catch (error) {
console.error('Error deleting supply:', error)
toast.error('Ошибка при удалении расходника')
// Удалить строку
const removeRow = async (supplyId: string, isNew: boolean) => {
if (isNew) {
// Просто удаляем из массива если это новая строка
setEditableSupplies(prev => prev.filter(s => s.id !== supplyId))
} else {
// Удаляем существующую запись сразу
try {
await deleteSupply({
variables: { id: supplyId },
update: (cache, { data }) => {
// Обновляем кэш Apollo Client
const existingData = cache.readQuery({ query: GET_MY_SUPPLIES }) as { mySupplies: Supply[] } | null
if (existingData && existingData.mySupplies) {
cache.writeQuery({
query: GET_MY_SUPPLIES,
data: {
mySupplies: existingData.mySupplies.filter((s: Supply) => s.id !== supplyId)
}
})
}
}
})
// Удаляем из локального состояния по ID, а не по индексу
setEditableSupplies(prev => prev.filter(s => s.id !== supplyId))
toast.success('Расходник успешно удален')
} catch (error) {
console.error('Error deleting supply:', error)
toast.error('Ошибка при удалении расходника')
}
}
}
// Начать редактирование существующей строки
const startEditing = (supplyId: string) => {
setEditableSupplies(prev =>
prev.map(supply =>
supply.id === supplyId ? { ...supply, isEditing: true } : supply
)
)
}
// Отменить редактирование
const cancelEditing = (supplyId: string) => {
const supply = editableSupplies.find(s => s.id === supplyId)
if (!supply) return
if (supply.isNew) {
// Удаляем новую строку
setEditableSupplies(prev => prev.filter(s => s.id !== supplyId))
} else {
// Возвращаем к исходному состоянию
const originalSupply = supplies.find((s: Supply) => s.id === supply.id)
if (originalSupply) {
setEditableSupplies(prev =>
prev.map(s =>
s.id === supplyId
? {
id: originalSupply.id,
name: originalSupply.name,
description: originalSupply.description || '',
price: originalSupply.price.toString(),
imageUrl: originalSupply.imageUrl || '',
isNew: false,
isEditing: false,
hasChanges: false
}
: s
)
)
}
}
}
// Обновить поле
const updateField = (supplyId: string, field: keyof EditableSupply, value: string | File) => {
setEditableSupplies(prev =>
prev.map((supply) => {
if (supply.id !== supplyId) return supply
const updated = { ...supply, hasChanges: true }
if (field === 'imageFile' && value instanceof File) {
updated.imageFile = value
updated.imageUrl = URL.createObjectURL(value)
} else if (typeof value === 'string') {
if (field === 'name') updated.name = value
else if (field === 'description') updated.description = value
else if (field === 'price') updated.price = value
else if (field === 'imageUrl') updated.imageUrl = value
}
return updated
})
)
}
// Загрузка изображения
const uploadImageAndGetUrl = async (file: File): Promise<string> => {
if (!user?.id) throw new Error('User not found')
@ -117,246 +217,184 @@ export function SuppliesTab() {
}
const result = await response.json()
console.log('Upload result:', result)
return result.url
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!formData.name.trim() || !formData.price || !formData.quantity) {
toast.error('Заполните обязательные поля')
return
}
const quantity = parseInt(formData.quantity)
if (quantity < 0) {
toast.error('Количество не может быть отрицательным')
return
}
// Сохранить все изменения
const saveAllChanges = async () => {
setIsSaving(true)
try {
let imageUrl = formData.imageUrl
// Загружаем изображение если выбрано
if (imageFile) {
const uploadResult = await uploadImageAndGetUrl(imageFile)
imageUrl = uploadResult
}
const input = {
name: formData.name,
description: formData.description || undefined,
price: parseFloat(formData.price),
quantity: quantity,
imageUrl: imageUrl || undefined
}
if (editingSupply) {
await updateSupply({
variables: { id: editingSupply.id, input }
})
} else {
await createSupply({
variables: { input }
})
}
await refetch()
setIsDialogOpen(false)
resetForm()
const suppliesToSave = editableSupplies.filter(s => {
if (s.isNew) {
// Для новых записей проверяем что обязательные поля заполнены
return s.name.trim() && s.price
}
// Для существующих записей проверяем флаг изменений
return s.hasChanges
})
toast.success(editingSupply ? 'Расходник успешно обновлен' : 'Расходник успешно создан')
console.log('Supplies to save:', suppliesToSave.length, suppliesToSave)
for (const supply of suppliesToSave) {
if (!supply.name.trim() || !supply.price) {
toast.error(`Заполните обязательные поля для всех расходников`)
setIsSaving(false)
return
}
let imageUrl = supply.imageUrl
// Загружаем изображение если выбрано
if (supply.imageFile) {
imageUrl = await uploadImageAndGetUrl(supply.imageFile)
}
const input = {
name: supply.name,
description: supply.description || undefined,
price: parseFloat(supply.price),
imageUrl: imageUrl || undefined
}
if (supply.isNew) {
await createSupply({
variables: { input },
update: (cache, { data }) => {
if (data?.createSupply?.supply) {
const existingData = cache.readQuery({ query: GET_MY_SUPPLIES }) as { mySupplies: Supply[] } | null
if (existingData) {
cache.writeQuery({
query: GET_MY_SUPPLIES,
data: {
mySupplies: [...existingData.mySupplies, data.createSupply.supply]
}
})
}
}
}
})
} else if (supply.id) {
await updateSupply({
variables: { id: supply.id, input },
update: (cache, { data }) => {
if (data?.updateSupply?.supply) {
const existingData = cache.readQuery({ query: GET_MY_SUPPLIES }) as { mySupplies: Supply[] } | null
if (existingData) {
cache.writeQuery({
query: GET_MY_SUPPLIES,
data: {
mySupplies: existingData.mySupplies.map((s: Supply) =>
s.id === data.updateSupply.supply.id ? data.updateSupply.supply : s
)
}
})
}
}
}
})
}
}
// Удаления теперь происходят сразу в removeRow, так что здесь обрабатываем только обновления
toast.success('Все изменения успешно сохранены')
setPendingChanges([])
} catch (error) {
console.error('Error saving supply:', error)
toast.error('Ошибка при сохранении расходника')
console.error('Error saving changes:', error)
toast.error('Ошибка при сохранении изменений')
} finally {
setIsSaving(false)
}
}
// Проверяем есть ли несохраненные изменения
const hasUnsavedChanges = useMemo(() => {
return editableSupplies.some(s => {
if (s.isNew) {
// Для новых записей проверяем что есть данные для сохранения
return s.name.trim() || s.price || s.description.trim()
}
return s.hasChanges
})
}, [editableSupplies])
return (
<div className="h-full flex flex-col">
<Card className="flex-1 bg-white/5 backdrop-blur border-white/10 p-6 overflow-hidden">
{/* Заголовок и кнопка добавления */}
{/* Заголовок и кнопки */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-white mb-1">Мои расходники</h2>
<p className="text-white/70 text-sm">Управление вашими расходными материалами</p>
<p className="text-white/70 text-sm">Управление вашими расходниками</p>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white border-0 shadow-lg shadow-purple-500/25 transition-all duration-300"
onClick={() => {
resetForm()
setIsDialogOpen(true)
}}
>
<Plus className="w-4 h-4 mr-2" />
Добавить расходник
</Button>
</DialogTrigger>
<div className="flex gap-3">
<Button
onClick={addNewRow}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300 hover:scale-105"
>
<Plus className="w-4 h-4 mr-2" />
Добавить расходник
</Button>
<DialogContent className="max-w-md bg-gradient-to-br from-purple-900/95 via-purple-800/95 to-pink-900/95 backdrop-blur-xl border border-purple-500/30 text-white shadow-2xl shadow-purple-500/20">
<DialogHeader className="pb-6">
<DialogTitle className="text-2xl font-bold bg-gradient-to-r from-purple-300 to-pink-300 bg-clip-text text-transparent">
{editingSupply ? 'Редактировать расходник' : 'Добавить расходник'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="name" className="text-purple-200 text-sm font-medium">Название расходника *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
className="bg-white/5 border-purple-400/30 text-white placeholder:text-purple-300/50 focus:border-purple-300 focus:ring-2 focus:ring-purple-500/20 transition-all"
placeholder="Введите название расходника"
required
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="price" className="text-purple-200 text-sm font-medium">Цена за единицу () *</Label>
<Input
id="price"
type="number"
step="0.01"
min="0"
value={formData.price}
onChange={(e) => setFormData(prev => ({ ...prev, price: e.target.value }))}
className="bg-white/5 border-purple-400/30 text-white placeholder:text-purple-300/50 focus:border-purple-300 focus:ring-2 focus:ring-purple-500/20 transition-all"
placeholder="0.00"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="quantity" className="text-purple-200 text-sm font-medium">Количество *</Label>
<Input
id="quantity"
type="number"
min="0"
value={formData.quantity}
onChange={(e) => setFormData(prev => ({ ...prev, quantity: e.target.value }))}
className="bg-white/5 border-purple-400/30 text-white placeholder:text-purple-300/50 focus:border-purple-300 focus:ring-2 focus:ring-purple-500/20 transition-all"
placeholder="0"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description" className="text-purple-200 text-sm font-medium">Описание</Label>
<Input
id="description"
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
className="bg-white/5 border-purple-400/30 text-white placeholder:text-purple-300/50 focus:border-purple-300 focus:ring-2 focus:ring-purple-500/20 transition-all"
placeholder="Описание расходника"
/>
</div>
<div className="space-y-2">
<Label className="text-purple-200 text-sm font-medium">Изображение</Label>
<Input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) {
setImageFile(file)
}
}}
className="bg-white/5 border-purple-400/30 text-white file:bg-gradient-to-r file:from-purple-500 file:to-pink-500 file:text-white file:border-0 file:rounded-lg file:px-4 file:py-2 file:mr-3 focus:border-purple-300 focus:ring-2 focus:ring-purple-500/20 transition-all"
/>
{formData.imageUrl && (
<div className="mt-3">
<Image
src={formData.imageUrl}
alt="Preview"
width={80}
height={80}
className="w-20 h-20 object-cover rounded-lg border border-purple-400/30 shadow-lg"
/>
</div>
)}
</div>
<div className="flex gap-3 pt-8">
<Button
type="button"
variant="outline"
onClick={() => {
setIsDialogOpen(false)
resetForm()
}}
className="flex-1 border-purple-400/30 text-purple-200 hover:bg-purple-500/10 hover:border-purple-300 transition-all duration-300"
>
Отмена
</Button>
<Button
type="submit"
disabled={loading || isUploading}
className="flex-1 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300"
>
{loading || isUploading ? 'Сохранение...' : 'Сохранить'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
{hasUnsavedChanges && (
<Button
onClick={saveAllChanges}
disabled={isSaving}
className="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white border-0 shadow-lg shadow-green-500/25 hover:shadow-green-500/40 transition-all duration-300 hover:scale-105 disabled:hover:scale-100"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? 'Сохранение...' : 'Сохранить все'}
</Button>
)}
</div>
</div>
{/* Таблица расходников */}
<div className="overflow-auto flex-1">
{loading ? (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-4 animate-spin">
<svg className="w-8 h-8 text-white/50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<p className="text-white/70 text-sm">Загрузка расходников...</p>
</div>
</div>
) : error ? (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-white mb-2">Ошибка загрузки</h3>
<p className="text-white/70 text-sm mb-4">
Не удалось загрузить расходники
</p>
<Button
onClick={() => refetch()}
className="bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white"
>
Попробовать снова
</Button>
</div>
</div>
) : supplies.length === 0 ? (
{/* Таблица расходников */}
<div className="overflow-auto flex-1">
{loading ? (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-4 animate-spin">
<svg className="w-8 h-8 text-white/50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<p className="text-white/70 text-sm">Загрузка расходников...</p>
</div>
</div>
) : error ? (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-white mb-2">Ошибка загрузки</h3>
<p className="text-white/70 text-sm mb-4">
Не удалось загрузить расходники
</p>
<Button
onClick={() => refetch()}
className="bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white"
>
Попробовать снова
</Button>
</div>
</div>
) : editableSupplies.length === 0 ? (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-4">
<Package className="w-8 h-8 text-white/50" />
<Plus className="w-8 h-8 text-white/50" />
</div>
<h3 className="text-lg font-semibold text-white mb-2">Пока нет расходников</h3>
<p className="text-white/70 text-sm mb-4">
Добавьте свой первый расходник для управления складскими запасами
Создайте свой первый расходник, чтобы начать работу
</p>
<Button
onClick={() => {
resetForm()
setIsDialogOpen(true)
}}
onClick={addNewRow}
className="bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white"
>
<Plus className="w-4 h-4 mr-2" />
@ -371,66 +409,184 @@ export function SuppliesTab() {
<tr>
<th className="text-left p-4 text-white font-medium"></th>
<th className="text-left p-4 text-white font-medium">Фото</th>
<th className="text-left p-4 text-white font-medium">Название</th>
<th className="text-left p-4 text-white font-medium">Цена за единицу</th>
<th className="text-left p-4 text-white font-medium">Количество</th>
<th className="text-left p-4 text-white font-medium">Название *</th>
<th className="text-left p-4 text-white font-medium">Цена за единицу () *</th>
<th className="text-left p-4 text-white font-medium">Описание</th>
<th className="text-left p-4 text-white font-medium">Действия</th>
</tr>
</thead>
<tbody>
{supplies.map((supply: Supply, index: number) => (
<tr key={supply.id} className="border-t border-white/10 hover:bg-white/5">
{editableSupplies.map((supply, index) => (
<tr key={supply.id || index} className={`border-t border-white/10 hover:bg-white/5 ${supply.isNew || supply.hasChanges ? 'bg-blue-500/10' : ''}`}>
<td className="p-4 text-white/80">{index + 1}</td>
<td className="p-4">
{supply.imageUrl ? (
<Image
src={supply.imageUrl}
alt={supply.name}
width={48}
height={48}
className="w-12 h-12 object-cover rounded border border-white/20"
/>
{/* Фото */}
<td className="p-4 relative">
{supply.isEditing ? (
<div className="flex items-center gap-3">
<Input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) {
updateField(supply.id!, 'imageFile', file)
}
}}
className="bg-white/5 border-white/20 text-white text-xs file:bg-gradient-to-r file:from-purple-500 file:to-pink-500 file:text-white file:border-0 file:rounded file:px-2 file:py-1 file:mr-2 file:text-xs flex-1"
/>
{supply.imageUrl && (
<div className="relative group w-12 h-12 flex-shrink-0">
<Image
src={supply.imageUrl}
alt="Preview"
width={48}
height={48}
className="w-12 h-12 object-cover rounded border border-white/20 cursor-pointer transition-all duration-300 group-hover:ring-2 group-hover:ring-purple-400/50"
/>
{/* Увеличенная версия при hover */}
<div className="absolute top-0 left-0 opacity-0 group-hover:opacity-100 transition-all duration-300 pointer-events-none z-50 transform group-hover:scale-100 scale-75">
<div className="relative">
<Image
src={supply.imageUrl}
alt="Preview"
width={200}
height={200}
className="w-50 h-50 object-cover rounded-lg border-2 border-purple-400 shadow-2xl shadow-purple-500/30 bg-black/90 backdrop-blur"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent rounded-lg"></div>
</div>
</div>
</div>
)}
</div>
) : supply.imageUrl ? (
<div className="relative group w-12 h-12">
<Image
src={supply.imageUrl}
alt={supply.name}
width={48}
height={48}
className="w-12 h-12 object-cover rounded border border-white/20 cursor-pointer transition-all duration-300 group-hover:ring-2 group-hover:ring-purple-400/50"
/>
{/* Увеличенная версия при hover */}
<div className="absolute top-0 left-0 opacity-0 group-hover:opacity-100 transition-all duration-300 pointer-events-none z-50 transform group-hover:scale-100 scale-75">
<div className="relative">
<Image
src={supply.imageUrl}
alt={supply.name}
width={240}
height={240}
className="w-60 h-60 object-cover rounded-lg border-2 border-purple-400 shadow-2xl shadow-purple-500/30 bg-black/90 backdrop-blur"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent rounded-lg"></div>
<div className="absolute bottom-2 left-2 right-2 bg-black/60 backdrop-blur rounded px-2 py-1">
<p className="text-white text-xs font-medium truncate">{supply.name}</p>
</div>
</div>
</div>
</div>
) : (
<div className="w-12 h-12 bg-white/10 rounded flex items-center justify-center">
<Upload className="w-5 h-5 text-white/50" />
</div>
)}
</td>
<td className="p-4 text-white font-medium">{supply.name}</td>
<td className="p-4 text-white/80">{supply.price.toLocaleString()} </td>
{/* Название */}
<td className="p-4">
<div className="flex items-center gap-2">
<span className="text-white/80">{supply.quantity} шт.</span>
{supply.quantity <= 10 && (
<span className="text-xs bg-yellow-500/20 text-yellow-400 px-2 py-1 rounded-full">
Мало
</span>
)}
{supply.quantity === 0 && (
<span className="text-xs bg-red-500/20 text-red-400 px-2 py-1 rounded-full">
Нет в наличии
</span>
)}
</div>
{supply.isEditing ? (
<Input
value={supply.name}
onChange={(e) => updateField(supply.id!, 'name', e.target.value)}
className="bg-white/5 border-white/20 text-white"
placeholder="Название расходника"
/>
) : (
<span className="text-white font-medium">{supply.name}</span>
)}
</td>
<td className="p-4 text-white/80">{supply.description || '—'}</td>
{/* Цена */}
<td className="p-4">
{supply.isEditing ? (
<Input
type="number"
step="0.01"
min="0"
value={supply.price}
onChange={(e) => updateField(supply.id!, 'price', e.target.value)}
className="bg-white/5 border-white/20 text-white"
placeholder="0.00"
/>
) : (
<span className="text-white/80">
{supply.price ? parseFloat(supply.price).toLocaleString() : '0'}
</span>
)}
</td>
{/* Описание */}
<td className="p-4">
{supply.isEditing ? (
<Input
value={supply.description}
onChange={(e) => updateField(supply.id!, 'description', e.target.value)}
className="bg-white/5 border-white/20 text-white"
placeholder="Описание расходника"
/>
) : (
<span className="text-white/80">{supply.description || '—'}</span>
)}
</td>
{/* Действия */}
<td className="p-4">
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleEdit(supply)}
className="border-white/20 text-white hover:bg-white/10"
>
<Edit className="w-4 h-4" />
</Button>
{supply.isEditing ? (
<>
<Button
size="sm"
onClick={() => {
// Сохраняем только этот расходник если заполнены обязательные поля
if (supply.name.trim() && supply.price) {
saveAllChanges()
} else {
toast.error('Заполните обязательные поля')
}
}}
disabled={!supply.name.trim() || !supply.price || isSaving}
className="h-8 w-8 p-0 bg-gradient-to-r from-green-500/20 to-emerald-500/20 hover:from-green-500/30 hover:to-emerald-500/30 border border-green-500/30 hover:border-green-400/50 text-green-300 hover:text-white transition-all duration-200 shadow-lg shadow-green-500/10 hover:shadow-green-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
title="Сохранить"
>
<Check className="w-4 h-4" />
</Button>
<Button
size="sm"
onClick={() => cancelEditing(supply.id!)}
className="h-8 w-8 p-0 bg-gradient-to-r from-red-500/20 to-red-600/20 hover:from-red-500/30 hover:to-red-600/30 border border-red-500/30 hover:border-red-400/50 text-red-300 hover:text-white transition-all duration-200 shadow-lg shadow-red-500/10 hover:shadow-red-500/20"
title="Отменить"
>
<X className="w-4 h-4" />
</Button>
</>
) : (
<Button
size="sm"
onClick={() => startEditing(supply.id!)}
className="h-8 w-8 p-0 bg-gradient-to-r from-purple-500/20 to-pink-500/20 hover:from-purple-500/30 hover:to-pink-500/30 border border-purple-500/30 hover:border-purple-400/50 text-purple-300 hover:text-white transition-all duration-200 shadow-lg shadow-purple-500/10 hover:shadow-purple-500/20"
title="Редактировать"
>
<Edit className="w-4 h-4" />
</Button>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/10"
className="h-8 w-8 p-0 bg-gradient-to-r from-red-500/20 to-red-600/20 hover:from-red-500/30 hover:to-red-600/30 border border-red-500/30 hover:border-red-400/50 text-red-300 hover:text-white transition-all duration-200 shadow-lg shadow-red-500/10 hover:shadow-red-500/20"
title="Удалить"
>
<Trash2 className="w-4 h-4" />
</Button>
@ -449,7 +605,7 @@ export function SuppliesTab() {
Отмена
</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDelete(supply.id)}
onClick={() => removeRow(supply.id!, supply.isNew)}
className="bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 text-white border-0 shadow-lg shadow-red-500/25 hover:shadow-red-500/40 transition-all duration-300"
>
Удалить
@ -467,6 +623,8 @@ export function SuppliesTab() {
)}
</div>
</Card>
</div>
)
}
}