Files
sfera/src/components/services/supplies-tab.tsx
Veronika Smirnova 0304f69410 Fix fulfillment consumables pricing architecture
- Add pricePerUnit field to Supply model for seller pricing
- Fix updateSupplyPrice mutation to update pricePerUnit only
- Separate purchase price (price) from selling price (pricePerUnit)
- Fix GraphQL mutations to include organization field (CREATE/UPDATE_LOGISTICS)
- Update GraphQL types to make Supply.price required again
- Add comprehensive pricing rules to rules-complete.md sections 11.7.5 and 18.8
- Fix supplies-tab.tsx to show debug info and handle user loading

Architecture changes:
• Supply.price = purchase price from supplier (immutable)
• Supply.pricePerUnit = selling price to sellers (mutable by fulfillment)
• Warehouse shows purchase price only (readonly)
• Services shows/edits selling price only

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 14:33:40 +03:00

466 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useQuery, useMutation } from '@apollo/client'
import { Package, Save, X, Edit, Check } from 'lucide-react'
import Image from 'next/image'
import { useState, useEffect, useMemo } from 'react'
import { toast } from 'sonner'
// 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 { UPDATE_SUPPLY_PRICE } from '@/graphql/mutations'
import { GET_MY_SUPPLIES } from '@/graphql/queries'
import { useAuth } from '@/hooks/useAuth'
interface Supply {
id: string
name: string
description?: string
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
name: string
description: string
pricePerUnit: string // Цена за единицу - единственное редактируемое поле
unit: string
imageUrl: string
warehouseStock: number
isAvailable: boolean
isEditing: boolean
hasChanges: boolean
}
// PendingChange interface no longer needed
export function SuppliesTab() {
const { user } = useAuth()
const [editableSupplies, setEditableSupplies] = useState<EditableSupply[]>([])
// No longer need pending changes tracking
const [isSaving, setIsSaving] = useState(false)
const [isInitialized, setIsInitialized] = useState(false)
// Debug информация
console.log('SuppliesTab - User:', user?.phone, 'Type:', user?.organization?.type)
// GraphQL запросы и мутации
const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, {
skip: !user || user?.organization?.type !== 'FULFILLMENT',
})
const [updateSupplyPrice] = useMutation(UPDATE_SUPPLY_PRICE)
// Debug GraphQL запроса
console.log('SuppliesTab - Query:', {
skip: !user || user?.organization?.type !== 'FULFILLMENT',
loading,
error: error?.message,
dataLength: data?.mySupplies?.length,
})
const supplies = data?.mySupplies || []
// Преобразуем загруженные расходники в редактируемый формат
useEffect(() => {
if (data?.mySupplies && !isInitialized) {
const convertedSupplies: EditableSupply[] = data.mySupplies.map((supply: Supply) => ({
id: supply.id,
name: supply.name,
description: supply.description || '',
pricePerUnit: supply.pricePerUnit ? supply.pricePerUnit.toString() : '',
unit: supply.unit,
imageUrl: supply.imageUrl || '',
warehouseStock: supply.warehouseStock,
isAvailable: supply.isAvailable,
isEditing: false,
hasChanges: false,
}))
setEditableSupplies(convertedSupplies)
setIsInitialized(true)
}
}, [data, isInitialized])
// Расходники нельзя создавать - они появляются автоматически со склада
// const addNewRow = () => { ... } - REMOVED
// Расходники нельзя удалять - они управляются через склад
// const removeRow = async (supplyId: string, isNew: boolean) => { ... } - REMOVED
// Начать редактирование существующей строки
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
// Возвращаем к исходному состоянию
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) => {
if (field !== 'pricePerUnit') {
return // Только цену можно редактировать
}
setEditableSupplies((prev) =>
prev.map((supply) => {
if (supply.id !== supplyId) return supply
return {
...supply,
pricePerUnit: value,
hasChanges: true,
}
}),
)
}
// Image upload no longer needed - supplies are readonly except price
// Сохранить все изменения (только цены)
const saveAllChanges = async () => {
setIsSaving(true)
try {
const suppliesToSave = editableSupplies.filter((s) => s.hasChanges)
for (const supply of suppliesToSave) {
// Проверяем валидность цены (может быть пустой)
const pricePerUnit = supply.pricePerUnit.trim() ? parseFloat(supply.pricePerUnit) : null
if (supply.pricePerUnit.trim() && (isNaN(pricePerUnit!) || pricePerUnit! <= 0)) {
toast.error('Введите корректную цену')
setIsSaving(false)
return
}
const input = {
pricePerUnit: pricePerUnit,
}
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,
),
},
})
}
}
},
})
}
// Сбрасываем флаги изменений
setEditableSupplies((prev) => prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })))
toast.success('Цены успешно обновлены')
} catch (error) {
console.error('Error saving changes:', error)
toast.error('Ошибка при сохранении цен')
} finally {
setIsSaving(false)
}
}
// Проверяем есть ли несохраненные изменения (только цены)
const hasUnsavedChanges = useMemo(() => {
return editableSupplies.some((s) => 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>
</div>
<div className="flex gap-3">
{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">
Не удалось загрузить расходники
{process.env.NODE_ENV === 'development' && (
<>
<br />
<span className="text-xs text-red-300">
Debug: {error.message}
<br />
User type: {user?.organization?.type}
</span>
</>
)}
</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" />
</div>
<h3 className="text-lg font-semibold text-white mb-2">Пока нет расходников</h3>
<p className="text-white/70 text-sm mb-4">Расходники появятся автоматически при получении поставок</p>
</div>
</div>
) : (
<div className="bg-white/5 rounded-lg border border-white/10 overflow-hidden">
<table className="w-full">
<thead className="bg-white/5">
<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>
</tr>
</thead>
<tbody>
{editableSupplies.map((supply, index) => (
<tr
key={supply.id || index}
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.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">
<Package className="w-5 h-5 text-white/50" />
</div>
)}
</td>
{/* Название */}
<td className="p-4">
<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.pricePerUnit}
onChange={(e) => updateField(supply.id, 'pricePerUnit', e.target.value)}
className="bg-white/5 border-white/20 text-white"
placeholder="Не установлена"
/>
) : (
<span className="text-white/80">
{supply.pricePerUnit
? `${parseFloat(supply.pricePerUnit).toLocaleString()}`
: 'Не установлена'}
</span>
)}
</td>
{/* Описание */}
<td className="p-4">
<span className="text-white/80">{supply.description || '—'}</span>
</td>
{/* Действия */}
<td className="p-4">
<div className="flex gap-2">
{supply.isEditing ? (
<>
<Button
size="sm"
onClick={() => {
saveAllChanges()
}}
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="Сохранить"
>
<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>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</Card>
</div>
)
}