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>
This commit is contained in:
@ -54,12 +54,23 @@ export function SuppliesTab() {
|
||||
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?.organization?.type !== 'FULFILLMENT',
|
||||
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 || []
|
||||
|
||||
// Преобразуем загруженные расходники в редактируемый формат
|
||||
@ -130,7 +141,7 @@ export function SuppliesTab() {
|
||||
if (field !== 'pricePerUnit') {
|
||||
return // Только цену можно редактировать
|
||||
}
|
||||
|
||||
|
||||
setEditableSupplies((prev) =>
|
||||
prev.map((supply) => {
|
||||
if (supply.id !== supplyId) return supply
|
||||
@ -155,7 +166,7 @@ export function SuppliesTab() {
|
||||
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)
|
||||
@ -187,9 +198,7 @@ export function SuppliesTab() {
|
||||
}
|
||||
|
||||
// Сбрасываем флаги изменений
|
||||
setEditableSupplies((prev) =>
|
||||
prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })),
|
||||
)
|
||||
setEditableSupplies((prev) => prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })))
|
||||
|
||||
toast.success('Цены успешно обновлены')
|
||||
} catch (error) {
|
||||
@ -212,7 +221,9 @@ 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>
|
||||
<p className="text-white/70 text-sm">
|
||||
Расходники появляются автоматически из поставок. Можно только установить цену.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
@ -261,7 +272,19 @@ export function SuppliesTab() {
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Ошибка загрузки</h3>
|
||||
<p className="text-white/70 text-sm mb-4">Не удалось загрузить расходники</p>
|
||||
<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"
|
||||
@ -301,9 +324,7 @@ export function SuppliesTab() {
|
||||
key={supply.id || index}
|
||||
className={`border-t border-white/10 hover:bg-white/5 ${
|
||||
supply.hasChanges ? 'bg-blue-500/10' : ''
|
||||
} ${
|
||||
supply.isAvailable ? '' : 'opacity-60'
|
||||
}`}
|
||||
} ${supply.isAvailable ? '' : 'opacity-60'}`}
|
||||
>
|
||||
<td className="p-4 text-white/80">{index + 1}</td>
|
||||
|
||||
@ -346,13 +367,13 @@ export function SuppliesTab() {
|
||||
<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'
|
||||
}`}>
|
||||
<span
|
||||
className={`text-sm font-medium ${supply.isAvailable ? 'text-green-400' : 'text-red-400'}`}
|
||||
>
|
||||
{supply.warehouseStock}
|
||||
</span>
|
||||
{!supply.isAvailable && (
|
||||
@ -362,7 +383,7 @@ export function SuppliesTab() {
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
||||
{/* Единица измерения */}
|
||||
<td className="p-4">
|
||||
<span className="text-white/80">{supply.unit}</span>
|
||||
@ -382,7 +403,9 @@ export function SuppliesTab() {
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80">
|
||||
{supply.pricePerUnit ? `${parseFloat(supply.pricePerUnit).toLocaleString()} ₽` : 'Не установлена'}
|
||||
{supply.pricePerUnit
|
||||
? `${parseFloat(supply.pricePerUnit).toLocaleString()} ₽`
|
||||
: 'Не установлена'}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
@ -650,7 +650,6 @@ export const UPDATE_SUPPLY_PRICE = gql`
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
// Мутация для заказа поставки расходников
|
||||
export const CREATE_SUPPLY_ORDER = gql`
|
||||
mutation CreateSupplyOrder($input: SupplyOrderInput!) {
|
||||
@ -746,6 +745,11 @@ export const CREATE_LOGISTICS = gql`
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
organization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -765,6 +769,11 @@ export const UPDATE_LOGISTICS = gql`
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
organization {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -718,7 +718,7 @@ export const resolvers = {
|
||||
console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', {
|
||||
organizationId: currentUser.organization.id,
|
||||
suppliesCount: transformedSupplies.length,
|
||||
supplies: transformedSupplies.map(s => ({
|
||||
supplies: transformedSupplies.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
pricePerUnit: s.pricePerUnit,
|
||||
@ -765,7 +765,7 @@ export const resolvers = {
|
||||
// Расходники фулфилмента из склада (новая архитектура - синхронизация со склада)
|
||||
myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => {
|
||||
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥')
|
||||
|
||||
|
||||
if (!context.user) {
|
||||
console.warn('❌ No user in context')
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
@ -826,7 +826,7 @@ export const resolvers = {
|
||||
})
|
||||
|
||||
// Преобразуем в формат для фронтенда
|
||||
return supplies.map(supply => ({
|
||||
return supplies.map((supply) => ({
|
||||
...supply,
|
||||
price: supply.price ? parseFloat(supply.price.toString()) : 0,
|
||||
shippedQuantity: 0, // Добавляем для совместимости
|
||||
@ -1387,7 +1387,6 @@ export const resolvers = {
|
||||
|
||||
// Мои товары и расходники (для поставщиков)
|
||||
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
||||
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
@ -1399,7 +1398,6 @@ export const resolvers = {
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
|
||||
if (!currentUser?.organization) {
|
||||
throw new GraphQLError('У пользователя нет организации')
|
||||
}
|
||||
@ -3607,7 +3605,6 @@ export const resolvers = {
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
|
||||
updateSupplyPrice: async (
|
||||
_: unknown,
|
||||
@ -3655,7 +3652,7 @@ export const resolvers = {
|
||||
const updatedSupply = await prisma.supply.update({
|
||||
where: { id: args.id },
|
||||
data: {
|
||||
price: args.input.pricePerUnit, // Обновляем только цену
|
||||
pricePerUnit: args.input.pricePerUnit, // Обновляем цену продажи, НЕ цену закупки
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
include: { organization: true },
|
||||
@ -4050,7 +4047,7 @@ export const resolvers = {
|
||||
return {
|
||||
name: product.name,
|
||||
description: product.description || `Заказано у ${partner.name}`,
|
||||
price: product.price,
|
||||
price: product.price, // Цена закупки у поставщика
|
||||
quantity: item.quantity,
|
||||
unit: 'шт',
|
||||
category: productWithCategory?.category?.name || 'Расходники',
|
||||
@ -5830,7 +5827,7 @@ export const resolvers = {
|
||||
data: {
|
||||
name: item.product.name,
|
||||
description: item.product.description || `Поставка от ${existingOrder.partner.name}`,
|
||||
price: item.price,
|
||||
price: item.price, // Цена закупки у поставщика
|
||||
quantity: item.quantity,
|
||||
unit: 'шт',
|
||||
category: item.product.category?.name || 'Расходники',
|
||||
@ -6590,7 +6587,7 @@ export const resolvers = {
|
||||
description: isSellerSupply
|
||||
? `Расходники селлера ${updatedOrder.organization?.name || updatedOrder.organization?.fullName}`
|
||||
: item.product.description || `Расходники от ${updatedOrder.partner.name}`,
|
||||
price: item.price,
|
||||
price: item.price, // Цена закупки у поставщика
|
||||
quantity: item.quantity,
|
||||
currentStock: item.quantity,
|
||||
usedStock: 0,
|
||||
|
@ -36,7 +36,7 @@ export const typeDefs = gql`
|
||||
|
||||
# Расходники селлеров (материалы клиентов)
|
||||
mySupplies: [Supply!]!
|
||||
|
||||
|
||||
# Доступные расходники для рецептур селлеров (только с ценой и в наличии)
|
||||
getAvailableSuppliesForRecipe: [SupplyForRecipe!]!
|
||||
|
||||
@ -522,25 +522,25 @@ export const typeDefs = gql`
|
||||
name: String!
|
||||
description: String
|
||||
# Новые поля для Services архитектуры
|
||||
pricePerUnit: Float # Цена за единицу для рецептур (может быть null)
|
||||
unit: String! # Единица измерения: "шт", "кг", "м"
|
||||
warehouseStock: Int! # Остаток на складе (readonly)
|
||||
isAvailable: Boolean! # Есть ли на складе (влияет на цвет)
|
||||
pricePerUnit: Float # Цена за единицу для рецептур (может быть null)
|
||||
unit: String! # Единица измерения: "шт", "кг", "м"
|
||||
warehouseStock: Int! # Остаток на складе (readonly)
|
||||
isAvailable: Boolean! # Есть ли на складе (влияет на цвет)
|
||||
warehouseConsumableId: ID! # Связь со складом
|
||||
# Поля из базы данных для обратной совместимости
|
||||
price: Float! # Из Prisma schema
|
||||
quantity: Int! # Из Prisma schema
|
||||
category: String! # Из Prisma schema
|
||||
status: String! # Из Prisma schema
|
||||
date: DateTime! # Из Prisma schema
|
||||
supplier: String! # Из Prisma schema
|
||||
minStock: Int! # Из Prisma schema
|
||||
currentStock: Int! # Из Prisma schema
|
||||
usedStock: Int! # Из Prisma schema
|
||||
type: String! # Из Prisma schema (SupplyType enum)
|
||||
sellerOwnerId: ID # Из Prisma schema
|
||||
sellerOwner: Organization # Из Prisma schema
|
||||
shopLocation: String # Из Prisma schema
|
||||
price: Float! # Цена закупки у поставщика (не меняется)
|
||||
quantity: Int! # Из Prisma schema
|
||||
category: String! # Из Prisma schema
|
||||
status: String! # Из Prisma schema
|
||||
date: DateTime! # Из Prisma schema
|
||||
supplier: String! # Из Prisma schema
|
||||
minStock: Int! # Из Prisma schema
|
||||
currentStock: Int! # Из Prisma schema
|
||||
usedStock: Int! # Из Prisma schema
|
||||
type: String! # Из Prisma schema (SupplyType enum)
|
||||
sellerOwnerId: ID # Из Prisma schema
|
||||
sellerOwner: Organization # Из Prisma schema
|
||||
shopLocation: String # Из Prisma schema
|
||||
imageUrl: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
@ -551,15 +551,15 @@ export const typeDefs = gql`
|
||||
type SupplyForRecipe {
|
||||
id: ID!
|
||||
name: String!
|
||||
pricePerUnit: Float! # Всегда не null
|
||||
pricePerUnit: Float! # Всегда не null
|
||||
unit: String!
|
||||
imageUrl: String
|
||||
warehouseStock: Int! # Всегда > 0
|
||||
warehouseStock: Int! # Всегда > 0
|
||||
}
|
||||
|
||||
# Для обновления цены расходника в разделе Услуги
|
||||
input UpdateSupplyPriceInput {
|
||||
pricePerUnit: Float # Может быть null (цена не установлена)
|
||||
pricePerUnit: Float # Может быть null (цена не установлена)
|
||||
}
|
||||
|
||||
input UseFulfillmentSuppliesInput {
|
||||
@ -567,7 +567,7 @@ export const typeDefs = gql`
|
||||
quantityUsed: Int!
|
||||
description: String # Описание использования (например, "Подготовка 300 продуктов")
|
||||
}
|
||||
|
||||
|
||||
# Устаревшие типы для обратной совместимости
|
||||
input SupplyInput {
|
||||
name: String!
|
||||
|
Reference in New Issue
Block a user