Обновления системы после анализа и оптимизации архитектуры
- Обновлена схема Prisma с новыми полями и связями - Актуализированы правила системы в rules-complete.md - Оптимизированы GraphQL типы, запросы и мутации - Улучшены компоненты интерфейса и валидация данных - Исправлены критические ESLint ошибки: удалены неиспользуемые импорты и переменные - Добавлены тестовые файлы для проверки функционала 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,26 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Plus, Trash2, Save, X, Edit, Upload, Check } from 'lucide-react'
|
||||
import { Package, Save, X, Edit, Check } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
// Alert dialog no longer needed for supplies
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { CREATE_SUPPLY, UPDATE_SUPPLY, DELETE_SUPPLY } from '@/graphql/mutations'
|
||||
import { UPDATE_SUPPLY_PRICE } from '@/graphql/mutations'
|
||||
import { GET_MY_SUPPLIES } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
@ -28,32 +18,39 @@ interface Supply {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
price: number
|
||||
pricePerUnit?: number | null
|
||||
unit: string
|
||||
imageUrl?: string
|
||||
warehouseStock: number
|
||||
isAvailable: boolean
|
||||
warehouseConsumableId: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
organization: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
interface EditableSupply {
|
||||
id?: string // undefined для новых записей
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
price: string
|
||||
pricePerUnit: string // Цена за единицу - единственное редактируемое поле
|
||||
unit: string
|
||||
imageUrl: string
|
||||
imageFile?: File
|
||||
isNew: boolean
|
||||
warehouseStock: number
|
||||
isAvailable: boolean
|
||||
isEditing: boolean
|
||||
hasChanges: boolean
|
||||
}
|
||||
|
||||
interface PendingChange extends EditableSupply {
|
||||
isDeleted?: boolean
|
||||
}
|
||||
// PendingChange interface no longer needed
|
||||
|
||||
export function SuppliesTab() {
|
||||
const { user } = useAuth()
|
||||
const [editableSupplies, setEditableSupplies] = useState<EditableSupply[]>([])
|
||||
const [pendingChanges, setPendingChanges] = useState<PendingChange[]>([])
|
||||
// No longer need pending changes tracking
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
|
||||
@ -61,9 +58,7 @@ export function SuppliesTab() {
|
||||
const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, {
|
||||
skip: user?.organization?.type !== 'FULFILLMENT',
|
||||
})
|
||||
const [createSupply] = useMutation(CREATE_SUPPLY)
|
||||
const [updateSupply] = useMutation(UPDATE_SUPPLY)
|
||||
const [deleteSupply] = useMutation(DELETE_SUPPLY)
|
||||
const [updateSupplyPrice] = useMutation(UPDATE_SUPPLY_PRICE)
|
||||
|
||||
const supplies = data?.mySupplies || []
|
||||
|
||||
@ -74,68 +69,25 @@ export function SuppliesTab() {
|
||||
id: supply.id,
|
||||
name: supply.name,
|
||||
description: supply.description || '',
|
||||
price: supply.price.toString(),
|
||||
pricePerUnit: supply.pricePerUnit ? supply.pricePerUnit.toString() : '',
|
||||
unit: supply.unit,
|
||||
imageUrl: supply.imageUrl || '',
|
||||
isNew: false,
|
||||
warehouseStock: supply.warehouseStock,
|
||||
isAvailable: supply.isAvailable,
|
||||
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: '',
|
||||
imageUrl: '',
|
||||
isNew: true,
|
||||
isEditing: true,
|
||||
hasChanges: false,
|
||||
}
|
||||
setEditableSupplies((prev) => [...prev, newRow])
|
||||
}
|
||||
// Расходники нельзя создавать - они появляются автоматически со склада
|
||||
// const addNewRow = () => { ... } - REMOVED
|
||||
|
||||
// Удалить строку
|
||||
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 removeRow = async (supplyId: string, isNew: boolean) => { ... } - REMOVED
|
||||
|
||||
// Начать редактирование существующей строки
|
||||
const startEditing = (supplyId: string) => {
|
||||
@ -149,172 +101,108 @@ export function SuppliesTab() {
|
||||
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 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 || '',
|
||||
pricePerUnit: originalSupply.pricePerUnit ? originalSupply.pricePerUnit.toString() : '',
|
||||
unit: originalSupply.unit,
|
||||
imageUrl: originalSupply.imageUrl || '',
|
||||
warehouseStock: originalSupply.warehouseStock,
|
||||
isAvailable: originalSupply.isAvailable,
|
||||
isEditing: false,
|
||||
hasChanges: false,
|
||||
}
|
||||
: s,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Обновить поле
|
||||
const updateField = (supplyId: string, field: keyof EditableSupply, value: string | File) => {
|
||||
// Обновить поле (только цену можно редактировать)
|
||||
const updateField = (supplyId: string, field: keyof EditableSupply, value: string) => {
|
||||
if (field !== 'pricePerUnit') {
|
||||
return // Только цену можно редактировать
|
||||
}
|
||||
|
||||
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 {
|
||||
...supply,
|
||||
pricePerUnit: value,
|
||||
hasChanges: true,
|
||||
}
|
||||
return updated
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Загрузка изображения
|
||||
const uploadImageAndGetUrl = async (file: File): Promise<string> => {
|
||||
if (!user?.id) throw new Error('User not found')
|
||||
// Image upload no longer needed - supplies are readonly except price
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('userId', user.id)
|
||||
formData.append('type', 'supply')
|
||||
|
||||
const response = await fetch('/api/upload-service-image', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to upload image')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.url
|
||||
}
|
||||
|
||||
// Сохранить все изменения
|
||||
// Сохранить все изменения (только цены)
|
||||
const saveAllChanges = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const suppliesToSave = editableSupplies.filter((s) => {
|
||||
if (s.isNew) {
|
||||
// Для новых записей проверяем что обязательные поля заполнены
|
||||
return s.name.trim() && s.price
|
||||
}
|
||||
// Для существующих записей проверяем флаг изменений
|
||||
return s.hasChanges
|
||||
})
|
||||
|
||||
console.warn('Supplies to save:', suppliesToSave.length, suppliesToSave)
|
||||
const suppliesToSave = editableSupplies.filter((s) => s.hasChanges)
|
||||
|
||||
for (const supply of suppliesToSave) {
|
||||
if (!supply.name.trim() || !supply.price) {
|
||||
toast.error('Заполните обязательные поля для всех расходников')
|
||||
// Проверяем валидность цены (может быть пустой)
|
||||
const pricePerUnit = supply.pricePerUnit.trim() ? parseFloat(supply.pricePerUnit) : null
|
||||
|
||||
if (supply.pricePerUnit.trim() && (isNaN(pricePerUnit!) || pricePerUnit! <= 0)) {
|
||||
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,
|
||||
pricePerUnit: pricePerUnit,
|
||||
}
|
||||
|
||||
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],
|
||||
},
|
||||
})
|
||||
}
|
||||
await updateSupplyPrice({
|
||||
variables: { id: supply.id, input },
|
||||
update: (cache, { data }) => {
|
||||
if (data?.updateSupplyPrice?.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.updateSupplyPrice.supply.id ? data.updateSupplyPrice.supply : s,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
} 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, так что здесь обрабатываем только обновления
|
||||
// Сбрасываем флаги изменений
|
||||
setEditableSupplies((prev) =>
|
||||
prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })),
|
||||
)
|
||||
|
||||
toast.success('Все изменения успешно сохранены')
|
||||
setPendingChanges([])
|
||||
toast.success('Цены успешно обновлены')
|
||||
} catch (error) {
|
||||
console.error('Error saving changes:', error)
|
||||
toast.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
|
||||
})
|
||||
return editableSupplies.some((s) => s.hasChanges)
|
||||
}, [editableSupplies])
|
||||
|
||||
return (
|
||||
@ -323,19 +211,11 @@ export function SuppliesTab() {
|
||||
{/* Заголовок и кнопки */}
|
||||
<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>
|
||||
<h2 className="text-lg font-semibold text-white mb-1">Расходники со склада</h2>
|
||||
<p className="text-white/70 text-sm">Расходники появляются автоматически из поставок. Можно только установить цену.</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
<Button
|
||||
onClick={saveAllChanges}
|
||||
@ -343,7 +223,7 @@ export function SuppliesTab() {
|
||||
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 ? 'Сохранение...' : 'Сохранить все'}
|
||||
{isSaving ? 'Обновление цен...' : 'Сохранить цены'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -394,17 +274,10 @@ export function SuppliesTab() {
|
||||
<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">
|
||||
<Plus className="w-8 h-8 text-white/50" />
|
||||
<Package 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={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" />
|
||||
Добавить расходник
|
||||
</Button>
|
||||
<p className="text-white/70 text-sm mb-4">Расходники появятся автоматически при получении поставок</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -414,8 +287,10 @@ 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>
|
||||
<th className="text-left p-4 text-white font-medium">Действия</th>
|
||||
</tr>
|
||||
@ -424,51 +299,17 @@ export function SuppliesTab() {
|
||||
{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' : ''}`}
|
||||
className={`border-t border-white/10 hover:bg-white/5 ${
|
||||
supply.hasChanges ? 'bg-blue-500/10' : ''
|
||||
} ${
|
||||
supply.isAvailable ? '' : 'opacity-60'
|
||||
}`}
|
||||
>
|
||||
<td className="p-4 text-white/80">{index + 1}</td>
|
||||
|
||||
{/* Фото */}
|
||||
<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 ? (
|
||||
{supply.imageUrl ? (
|
||||
<div className="relative group w-12 h-12">
|
||||
<Image
|
||||
src={supply.imageUrl}
|
||||
@ -496,56 +337,59 @@ export function SuppliesTab() {
|
||||
</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" />
|
||||
<Package className="w-5 h-5 text-white/50" />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Название */}
|
||||
<td className="p-4">
|
||||
{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>
|
||||
)}
|
||||
<span className="text-white font-medium">{supply.name}</span>
|
||||
</td>
|
||||
|
||||
{/* Остаток на складе */}
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm font-medium ${
|
||||
supply.isAvailable ? 'text-green-400' : 'text-red-400'
|
||||
}`}>
|
||||
{supply.warehouseStock}
|
||||
</span>
|
||||
{!supply.isAvailable && (
|
||||
<span className="px-2 py-1 rounded-full bg-red-500/20 text-red-300 text-xs">
|
||||
Нет в наличии
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Единица измерения */}
|
||||
<td className="p-4">
|
||||
<span className="text-white/80">{supply.unit}</span>
|
||||
</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)}
|
||||
value={supply.pricePerUnit}
|
||||
onChange={(e) => updateField(supply.id, 'pricePerUnit', e.target.value)}
|
||||
className="bg-white/5 border-white/20 text-white"
|
||||
placeholder="0.00"
|
||||
placeholder="Не установлена"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80">
|
||||
{supply.price ? parseFloat(supply.price).toLocaleString() : '0'} ₽
|
||||
{supply.pricePerUnit ? `${parseFloat(supply.pricePerUnit).toLocaleString()} ₽` : 'Не установлена'}
|
||||
</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>
|
||||
)}
|
||||
<span className="text-white/80">{supply.description || '—'}</span>
|
||||
</td>
|
||||
|
||||
{/* Действия */}
|
||||
@ -556,14 +400,9 @@ export function SuppliesTab() {
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Сохраняем только этот расходник если заполнены обязательные поля
|
||||
if (supply.name.trim() && supply.price) {
|
||||
saveAllChanges()
|
||||
} else {
|
||||
toast.error('Заполните обязательные поля')
|
||||
}
|
||||
saveAllChanges()
|
||||
}}
|
||||
disabled={!supply.name.trim() || !supply.price || isSaving}
|
||||
disabled={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="Сохранить"
|
||||
>
|
||||
@ -571,7 +410,7 @@ export function SuppliesTab() {
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => cancelEditing(supply.id!)}
|
||||
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="Отменить"
|
||||
>
|
||||
@ -581,47 +420,13 @@ export function SuppliesTab() {
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => startEditing(supply.id!)}
|
||||
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="Редактировать"
|
||||
title="Редактировать цену"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
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>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent className="bg-gradient-to-br from-red-900/95 via-red-800/95 to-red-900/95 backdrop-blur-xl border border-red-500/30 text-white shadow-2xl shadow-red-500/20">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-xl font-bold bg-gradient-to-r from-red-300 to-red-300 bg-clip-text text-transparent">
|
||||
Подтвердите удаление
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-red-200">
|
||||
Вы действительно хотите удалить расходник “{supply.name}”? Это действие
|
||||
необратимо.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="gap-3">
|
||||
<AlertDialogCancel className="border-red-400/30 text-red-200 hover:bg-red-500/10 hover:border-red-300 transition-all duration-300">
|
||||
Отмена
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
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"
|
||||
>
|
||||
Удалить
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
Reference in New Issue
Block a user