Merge: Объединение изменений навигации сайдбара с удаленными обновлениями

- Решен конфликт в fulfillment-supplies-dashboard.tsx
- Сохранены изменения персистентного сайдбара без импорта Sidebar
- Объединены обновления GraphQL схемы и других компонентов

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Bivekich
2025-08-07 19:27:27 +03:00
34 changed files with 4062 additions and 1482 deletions

1988
dev.log Normal file
View File

@ -0,0 +1,1988 @@
> sferav@0.1.0 dev
> next dev --turbopack
▲ Next.js 15.4.1 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.0.104:3000
- Environments: .env
- Experiments (use with caution):
· optimizePackageImports
✓ Starting...
✓ Ready in 834ms
○ Compiling /api/graphql ...
✓ Compiled /api/graphql in 1205ms
🚀 Проверка инициализации базы данных...
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
✨ Инициализация базы данных завершена
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 3994ms
POST /api/graphql 200 in 1067ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 742ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 813ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 1032ms
POST /api/graphql 200 in 539ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 1335ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 973ms
POST /api/graphql 200 in 696ms
○ Compiling /fulfillment-supplies ...
✓ Compiled /fulfillment-supplies in 1797ms
GET /fulfillment-supplies 200 in 1883ms
GET /fulfillment-supplies 200 in 79ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetMe',
query: 'query GetMe {\n' +
' me {\n' +
' id\n' +
' phone\n' +
' avatar\n' +
' managerName\n' +
' createdAt\n' +
' organization {\n' +
' id\n' +
' inn\n' +
' kpp\n' +
' name\n' +
' fullName\n' +
' address\n' +
' addressFull\n' +
' ogrn\n' +
' ...'
}
✓ Compiled /favicon.ico in 478ms
POST /api/graphql 200 in 656ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 750ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetMe',
query: 'query GetMe {\n' +
' me {\n' +
' id\n' +
' phone\n' +
' avatar\n' +
' managerName\n' +
' createdAt\n' +
' organization {\n' +
' id\n' +
' inn\n' +
' kpp\n' +
' name\n' +
' fullName\n' +
' address\n' +
' addressFull\n' +
' ogrn\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 320ms
POST /api/graphql 200 in 556ms
○ Compiling /fulfillment-warehouse ...
✓ Compiled /fulfillment-warehouse in 807ms
GET /fulfillment-warehouse 200 in 869ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetMe',
query: 'query GetMe {\n' +
' me {\n' +
' id\n' +
' phone\n' +
' avatar\n' +
' managerName\n' +
' createdAt\n' +
' organization {\n' +
' id\n' +
' inn\n' +
' kpp\n' +
' name\n' +
' fullName\n' +
' address\n' +
' addressFull\n' +
' ogrn\n' +
' ...'
}
GET /favicon.ico?favicon.45db1c09.ico 200 in 258ms
POST /api/graphql 200 in 1231ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetMe',
query: 'query GetMe {\n' +
' me {\n' +
' id\n' +
' phone\n' +
' avatar\n' +
' managerName\n' +
' createdAt\n' +
' organization {\n' +
' id\n' +
' inn\n' +
' kpp\n' +
' name\n' +
' fullName\n' +
' address\n' +
' addressFull\n' +
' ogrn\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetWarehouseProducts',
query: 'query GetWarehouseProducts {\n' +
' warehouseProducts {\n' +
' id\n' +
' name\n' +
' article\n' +
' description\n' +
' price\n' +
' quantity\n' +
' type\n' +
' category {\n' +
' id\n' +
' name\n' +
' __typename\n' +
' }\n' +
' brand\n' +
' c...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetMyCounterparties',
query: 'query GetMyCounterparties {\n' +
' myCounterparties {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' managementName\n' +
' type\n' +
' address\n' +
' market\n' +
' phones\n' +
' emails\n' +
' createdAt\n' +
' users {\n' +
' id\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
❌ GraphQL Errors: [
{
message: 'Cannot query field "price" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "quantity" on type "Supply". Did you mean "unit"?',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "category" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "status" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "date" on type "Supply". Did you mean "name"?',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "supplier" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "minStock" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "currentStock" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "usedStock" on type "Supply".',
locations: [ [Object] ],
path: undefined
}
]
POST /api/graphql 400 in 90ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetSupplyOrders',
query: 'query GetSupplyOrders {\n' +
' supplyOrders {\n' +
' id\n' +
' organizationId\n' +
' partnerId\n' +
' deliveryDate\n' +
' status\n' +
' totalAmount\n' +
' totalItems\n' +
' fulfillmentCenterId\n' +
' createdAt\n' +
' updatedAt\n' +
' part...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
❌ GraphQL Errors: [
{
message: 'Cannot query field "price" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "quantity" on type "Supply". Did you mean "unit"?',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "category" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "status" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "date" on type "Supply". Did you mean "name"?',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "supplier" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "minStock" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "currentStock" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "usedStock" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "type" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "shopLocation" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "sellerOwner" on type "Supply".',
locations: [ [Object] ],
path: undefined
}
]
POST /api/graphql 400 in 130ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetFulfillmentWarehouseStats',
query: 'query GetFulfillmentWarehouseStats {\n' +
' fulfillmentWarehouseStats {\n' +
' products {\n' +
' current\n' +
' change\n' +
' percentChange\n' +
' __typename\n' +
' }\n' +
' goods {\n' +
' current\n' +
' change\n' +
' per...'
}
🔥 FULFILLMENT WAREHOUSE STATS RESOLVER CALLED
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 3124ms
🏢 Organization ID: cmdzn23nl0002y5i4tytjh0ni, Date 24h ago: 2025-08-05T20:02:43.822Z
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
🔍 Резолвер warehouseProducts (доставленные заказы): {
currentUserId: 'cmdzn1sof0001y5i4h6fyp5jw',
organizationId: 'cmdzn23nl0002y5i4tytjh0ni',
organizationType: 'FULFILLMENT',
deliveredOrdersCount: 1,
orders: [
{
id: 'cme0dt4i80009y52fe5r82wr9',
sellerName: 'ФУЛФИЛМЕНТ РУ',
supplierName: 'ПОСТАВЩИК-ЭК',
status: 'DELIVERED',
itemsCount: 1,
deliveryDate: 2025-08-07T00:00:00.000Z
}
]
}
📦 Заказ от селлера ФУЛФИЛМЕНТ РУ у поставщика ПОСТАВЩИК-ЭК: [
{
productId: 'cmdztzlbn0001y5gr4pg0j0df',
productName: 'Пакет',
article: 'SF-C-446739-386',
orderedQuantity: 100,
price: 10
}
]
🚫 Исключен расходник из основного склада фулфилмента: {
name: 'Пакет',
type: 'CONSUMABLE',
orderId: 'cme0dt4i80009y52fe5r82wr9'
}
✅ Итого товаров на складе фулфилмента (из доставленных заказов): 0
POST /api/graphql 200 in 5813ms
POST /api/graphql 200 in 6353ms
📦 ALL DELIVERED ORDERS: 1
Order cme0dt4i80009y52fe5r82wr9: org=cmdzn23nl0002y5i4tytjh0ni (ФУЛФИЛМЕНТ РУ), fulfillment=cmdzn23nl0002y5i4tytjh0ni, items=1
🛒 SELLER ORDERS TO FULFILLMENT: 0
POST /api/graphql 200 in 3597ms
POST /api/graphql 200 in 7417ms
POST /api/graphql 200 in 2433ms
POST /api/graphql 200 in 6048ms
🏭 FULFILLMENT SUPPLY ORDERS: 1
🔥 FULFILLMENT SUPPLIES DEBUG: organizationId=cmdzn23nl0002y5i4tytjh0ni, ordersCount=1, warehouseCount=2, totalStock=100
📦 FULFILLMENT SUPPLIES BREAKDOWN: [
{
name: 'скотч10',
currentStock: 0,
supplier: 'Внутренний поставщик'
},
{ name: 'Пакет', currentStock: 100, supplier: 'ПОСТАВЩИК-ЭК' }
]
POST /api/graphql 200 in 7609ms
📊 FULFILLMENT SUPPLIES RECEIVED TODAY (ПРИБЫЛО): 1 orders, 100 items
💼 SELLER SUPPLIES DEBUG: totalCount=0 (from Supply warehouse)
📊 SELLER SUPPLIES RECEIVED TODAY: 0 supplies, 0 items
🏁 FINAL WAREHOUSE STATS RESULT: {
"products": {
"current": 0,
"change": 0,
"percentChange": 0
},
"goods": {
"current": 0,
"change": 0,
"percentChange": 0
},
"defects": {
"current": 0,
"change": 0,
"percentChange": 0
},
"pvzReturns": {
"current": 0,
"change": 0,
"percentChange": 0
},
"fulfillmentSupplies": {
"current": 100,
"change": 100,
"percentChange": 100
},
"sellerSupplies": {
"current": 0,
"change": 0,
"percentChange": 0
}
}
POST /api/graphql 200 in 8453ms
○ Compiling /fulfillment-warehouse/supplies ...
✓ Compiled /fulfillment-warehouse/supplies in 1604ms
GET /fulfillment-warehouse/supplies 200 in 1691ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
❌ GraphQL Errors: [
{
message: 'Cannot query field "price" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "quantity" on type "Supply". Did you mean "unit"?',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "category" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "status" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "date" on type "Supply". Did you mean "name"?',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "supplier" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "minStock" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "currentStock" on type "Supply".',
locations: [ [Object] ],
path: undefined
},
{
message: 'Cannot query field "usedStock" on type "Supply".',
locations: [ [Object] ],
path: undefined
}
]
POST /api/graphql 400 in 117ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetMe',
query: 'query GetMe {\n' +
' me {\n' +
' id\n' +
' phone\n' +
' avatar\n' +
' managerName\n' +
' createdAt\n' +
' organization {\n' +
' id\n' +
' inn\n' +
' kpp\n' +
' name\n' +
' fullName\n' +
' address\n' +
' addressFull\n' +
' ogrn\n' +
' ...'
}
GET /favicon.ico?favicon.45db1c09.ico 200 in 356ms
POST /api/graphql 200 in 561ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 1471ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 1015ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 607ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 610ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 925ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 951ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 365ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 457ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 1215ms
POST /api/graphql 200 in 610ms
POST /api/graphql 200 in 693ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 296ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 1596ms
POST /api/graphql 200 in 625ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 974ms
🚀 Проверка инициализации базы данных...
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 1304ms
✨ Инициализация базы данных завершена
POST /api/graphql 200 in 1651ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 691ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 982ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 1055ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 283ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 596ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 1552ms
POST /api/graphql 200 in 313ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 597ms
POST /api/graphql 200 in 650ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 1584ms
POST /api/graphql 200 in 701ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 702ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 729ms
POST /api/graphql 200 in 1124ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 603ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 948ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 801ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 306ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 409ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 307ms
POST /api/graphql 200 in 1266ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 833ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 658ms
POST /api/graphql 200 in 627ms
POST /api/graphql 200 in 1508ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 1040ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 1117ms
POST /api/graphql 200 in 692ms
POST /api/graphql 200 in 698ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 790ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 831ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 385ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 375ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetIncomingRequests',
query: 'query GetIncomingRequests {\n' +
' incomingRequests {\n' +
' id\n' +
' status\n' +
' message\n' +
' createdAt\n' +
' sender {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' phones\n' +
' email...'
}
POST /api/graphql 200 in 279ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 1908ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjFzb2YwMDAxeTVpNGg2ZnlwNWp3IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NDQ4NzQ4MSwiZXhwIjoxNzU3MDc5NDgxfQ.9KeIWoNPtDJNEU_SCoCba1ducS2pEpyhplg3YswCED4
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn1sof0001y5i4h6fyp5jw', phone: '79999999999' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjhkYjAwMDBoeTVpNHpveHp6ZmZnIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NDQ2NTExOCwiZXhwIjoxNzU3MDU3MTE4fQ.VP8LZUaONciSW9qBAVAjHVsY1lCpyiBVkVTcGoDaOGI
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn8db0000hy5i4zoxzzffg', phone: '78888888888' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjVwYWUwMDA5eTVpNHB5YXNpYnJqIiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NDUwOTc4MywiZXhwIjoxNzU3MTAxNzgzfQ.uC19oz6DE323E34mzAW7cZxw0vUjTbzRMktghrt5qgc
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn5pae0009y5i4pyasibrj', phone: '76666666666' }
🔍 GraphQL Operation: {
operationName: 'GetPendingSuppliesCount',
query: 'query GetPendingSuppliesCount {\n' +
' pendingSuppliesCount {\n' +
' supplyOrders\n' +
' ourSupplyOrders\n' +
' sellerSupplyOrders\n' +
' incomingSupplierOrders\n' +
' incomingRequests\n' +
' total\n' +
' __typename\n' +
' }\n' +
'}...'
}
POST /api/graphql 200 in 634ms
POST /api/graphql 200 in 1902ms
POST /api/graphql 200 in 2531ms
POST /api/graphql 200 in 1789ms
GraphQL Context - Auth header: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWR6bjJvOGowMDA0eTVpNDgxMHc4bzVjIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NDQ2NDg1MiwiZXhwIjoxNzU3MDU2ODUyfQ.sa2a5qIIOzJsgWJkC5qezQ6m4-JvwtxOKyEmHIiJ9zU
GraphQL Context - Token: eyJhbGciOiJIUzI1NiIs...
GraphQL Context - Decoded user: { id: 'cmdzn2o8j0004y5i4810w8o5c', phone: '77777777777' }
🔍 GraphQL Operation: {
operationName: 'GetConversations',
query: 'query GetConversations {\n' +
' conversations {\n' +
' id\n' +
' counterparty {\n' +
' id\n' +
' inn\n' +
' name\n' +
' fullName\n' +
' type\n' +
' address\n' +
' users {\n' +
' id\n' +
' avatar\n' +
' managerName\n' +
' ...'
}
POST /api/graphql 200 in 1464ms
🚀 Проверка инициализации базы данных...
POST /api/graphql 200 in 1162ms
POST /api/graphql 200 in 1638ms
✨ Инициализация базы данных завершена
POST /api/graphql 200 in 1317ms
POST /api/graphql 200 in 1189ms
POST /api/graphql 200 in 961ms
POST /api/graphql 200 in 375ms
POST /api/graphql 200 in 387ms
POST /api/graphql 200 in 321ms
POST /api/graphql 200 in 1497ms
○ Compiling /supplies/create-suppliers ...
✓ Compiled /supplies/create-suppliers in 556ms
GET /supplies/create-suppliers 200 in 793ms
POST /api/graphql 200 in 439ms
POST /api/graphql 200 in 320ms
POST /api/graphql 200 in 300ms
POST /api/graphql 200 in 350ms
POST /api/graphql 200 in 929ms
POST /api/graphql 200 in 1212ms
POST /api/graphql 200 in 1735ms
POST /api/graphql 200 in 638ms
POST /api/graphql 200 in 1867ms
POST /api/graphql 200 in 1048ms
POST /api/graphql 200 in 1179ms
POST /api/graphql 200 in 833ms
POST /api/graphql 200 in 1510ms
POST /api/graphql 200 in 657ms
POST /api/graphql 200 in 1168ms
POST /api/graphql 200 in 1187ms
POST /api/graphql 200 in 353ms
POST /api/graphql 200 in 395ms
POST /api/graphql 200 in 1481ms
POST /api/graphql 200 in 1100ms
POST /api/graphql 200 in 767ms
GET /supplies/create-suppliers 200 in 82ms
GET /favicon.ico 200 in 55ms
POST /api/graphql 200 in 560ms
POST /api/graphql 200 in 1414ms
POST /api/graphql 200 in 368ms
POST /api/graphql 200 in 372ms
POST /api/graphql 200 in 231ms
POST /api/graphql 200 in 1193ms
POST /api/graphql 200 in 397ms
POST /api/graphql 200 in 912ms
POST /api/graphql 200 in 971ms
POST /api/graphql 200 in 826ms
POST /api/graphql 200 in 616ms
○ Compiling /settings ...
✓ Compiled /settings in 689ms
GET /settings 200 in 822ms
POST /api/graphql 200 in 515ms
POST /api/graphql 200 in 301ms
POST /api/graphql 200 in 960ms
POST /api/graphql 200 in 692ms
POST /api/graphql 200 in 617ms
POST /api/graphql 200 in 777ms
POST /api/graphql 200 in 671ms
POST /api/graphql 200 in 291ms

View File

@ -12,6 +12,14 @@ const compat = new FlatCompat({
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
".next/**/*",
"node_modules/**/*",
"build/**/*",
"dist/**/*",
"*.config.js",
"*.config.mjs"
],
rules: {
// TypeScript правила
"@typescript-eslint/no-explicit-any": "warn",

View File

@ -65,6 +65,7 @@ model Organization {
ogrn String?
ogrnDate DateTime?
type OrganizationType
market String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
address String?
@ -202,7 +203,8 @@ model Supply {
id String @id @default(cuid())
name String
description String?
price Decimal @db.Decimal(10, 2)
price Decimal @db.Decimal(10, 2) // Цена закупки у поставщика (не меняется)
pricePerUnit Decimal? @db.Decimal(10, 2) // Цена продажи селлерам (устанавливается фулфилментом)
quantity Int @default(0)
unit String @default("шт")
category String @default("Расходники")

View File

@ -1,16 +1,19 @@
# ПРАВИЛА СИСТЕМЫ УПРАВЛЕНИЯ СКЛАДАМИ И ПОСТАВКАМИ - ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ v9.2
# ПРАВИЛА СИСТЕМЫ УПРАВЛЕНИЯ СКЛАДАМИ И ПОСТАВКАМИ - ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ v10.0
> ⚠️ **АБСОЛЮТНО ПОЛНЫЙ ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ**: Данный файл объединяет АБСОЛЮТНО ВСЕ правила системы: протоколы работы Claude Code, детальные протоколы по сложности, систему предотвращения нарушений, расширенную самопроверку, специальный UI/UX протокол и бизнес-правила. Визуальные правила вынесены в отдельный файл visual-design-rules.md с автоматической интеграцией.
## 🔴 ПРОТОКОЛЫ РАБОТЫ CLAUDE CODE
### ⛔ ЖЕСТКИЙ ПРОТОКОЛ - ОБЯЗАТЕЛЬНОЕ ИСПОЛНЕНИЕ
**Я НЕ МОГУ выполнять НИКАКИХ изменений в коде без предварительного чтения этого файла**
**КОМАНДА ОСТАНОВКИ**: "СТОП - ЧИТАЙ ПРАВИЛА" - немедленно останавливает любую работу
### 📋 ОБЯЗАТЕЛЬНЫЙ ЧЕК-ЛИСТ ПЕРЕД КАЖДОЙ ЗАДАЧЕЙ
**КАЖДЫЙ ОТВЕТ ДОЛЖЕН НАЧИНАТЬСЯ С:**
```
## 📋 Чек-лист соответствия правилам:
- ✅ Прочитал rules-complete.md
@ -19,16 +22,20 @@
- ✅ [ЕСЛИ UI/UX ЗАДАЧА] Прочитал visual-design-rules.md
- ✅ Готов выполнять согласно единому источнику истины
```
**БЕЗ ЭТОГО ЧЕК-ЛИСТА = НИКАКИХ ДЕЙСТВИЙ**
### 🔄 ДВУХЭТАПНЫЙ ПРОЦЕСС РАБОТЫ
#### **ЭТАП 1: ПЛАНИРОВАНИЕ (ОБЯЗАТЕЛЬНЫЙ)**
- Прочитать этот файл правил
- Создать детальный план действий
- Указать какие правила будут применены
- **ОСТАНОВИТЬСЯ И ЖДАТЬ ОДОБРЕНИЯ ПЛАНА**
#### **ЭТАП 2: ВЫПОЛНЕНИЕ (ТОЛЬКО ПОСЛЕ ОДОБРЕНИЯ)**
- Получить одобрение плана от пользователя
- Следовать ТОЛЬКО одобренному плану
- Использовать TodoWrite для отслеживания прогресса
@ -40,6 +47,7 @@
### 🎯 ПРОТОКОЛ ДЛЯ ЗАДАЧ СРЕДНЕЙ СЛОЖНОСТИ
**ОПРЕДЕЛЕНИЕ СРЕДНЕЙ СЛОЖНОСТИ:**
- Работа с 2-3 файлами
- Изменение логики в 1-2 модулях
- Добавление новых функций без изменения архитектуры
@ -48,6 +56,7 @@
**ОБЯЗАТЕЛЬНЫЕ ЭТАПЫ:**
#### 1. 🔍 **ЭТАП АНАЛИЗА** (STOP & THINK)
```
ПЕРЕД НАЧАЛОМ ЗАДАТЬ СЕБЕ:
□ Какие файлы нужно изучить? (перечислить ВСЕ)
@ -58,6 +67,7 @@
```
#### 2. 📋 **СОЗДАНИЕ ПЛАНА**
```
□ Разбить задачу на подзадачи (не более 5)
□ Определить порядок выполнения
@ -66,6 +76,7 @@
```
#### 3. 🔄 **ВЫПОЛНЕНИЕ С ПРОВЕРКАМИ**
```
ПОСЛЕ КАЖДОГО ШАГА:
□ Соответствует ли результат правилам из этого документа?
@ -77,6 +88,7 @@
### 🔥 ПРОТОКОЛ ДЛЯ ЗАДАЧ ВЫСОКОЙ СЛОЖНОСТИ
**ОПРЕДЕЛЕНИЕ ВЫСОКОЙ СЛОЖНОСТИ:**
- Работа с 4+ файлами
- Изменение архитектуры системы
- Создание новых модулей/компонентов
@ -86,6 +98,7 @@
**ОБЯЗАТЕЛЬНЫЕ ЭТАПЫ:**
#### 1. 🛑 **СТОП! ГЛУБОКИЙ АНАЛИЗ**
```
ОБЯЗАТЕЛЬНЫЕ ВОПРОСЫ ПОЛЬЗОВАТЕЛЮ:
□ Уточнить ВСЕ требования и ожидания
@ -95,6 +108,7 @@
```
#### 2. 🔍 **ИССЛЕДОВАТЕЛЬСКАЯ ФАЗА**
```
□ Изучить ВСЕ связанные файлы параллельно
□ Построить карту зависимостей
@ -104,6 +118,7 @@
```
#### 3. 📊 **СОЗДАНИЕ ДЕТАЛЬНОГО ПЛАНА**
```
□ Разбить на этапы с промежуточными проверками
□ Определить точки возврата (rollback points)
@ -114,6 +129,7 @@
### ❓ СИСТЕМА ОБЯЗАТЕЛЬНЫХ УТОЧНЕНИЙ
#### 🔴 **КРИТИЧЕСКИЕ СИТУАЦИИ** (ОБЯЗАТЕЛЬНО):
- Обнаружил противоречие в правилах
- Задача может нарушить архитектуру системы
- Неясно как применить правило к конкретной ситуации
@ -121,6 +137,7 @@
- Изменения затрагивают критические бизнес-процессы
#### 🟡 **ВАЖНЫЕ СИТУАЦИИ** (РЕКОМЕНДУЕТСЯ):
- Задача требует создания новых типов данных
- Нужно изменить существующий workflow
- Есть сомнения в интерпретации требований
@ -128,6 +145,7 @@
- Требуется интеграция с внешними системами
**ФОРМАТ УТОЧНЯЮЩИХ ВОПРОСОВ:**
```
🎯 КОНТЕКСТ: Что именно я делаю
❓ ВОПРОС: Что конкретно неясно
@ -143,6 +161,7 @@
**ОБЯЗАТЕЛЬНЫЕ ЭТАПЫ:**
#### 1. 📖 **ИЗУЧЕНИЕ ВИЗУАЛЬНЫХ ПРАВИЛ**
```
ОБЯЗАТЕЛЬНО:
□ Прочитать visual-design-rules.md
@ -152,6 +171,7 @@
```
#### 2. 🎯 **ПРИМЕНЕНИЕ ДИЗАЙН-СИСТЕМЫ**
```
ПРОВЕРИТЬ:
□ Соответствие цветовой палитре (OKLCH)
@ -162,6 +182,7 @@
```
#### 3. ✅ **ВАЛИДАЦИЯ ДИЗАЙНА**
```
УБЕДИТЬСЯ:
□ Соблюдены принципы иерархии
@ -175,6 +196,7 @@
### 🛑 ОБЯЗАТЕЛЬНЫЕ ОСТАНОВКИ ПЕРЕД ДЕЙСТВИЯМИ
#### **СТОП-СИГНАЛ #1: ПЕРЕД ЛЮБЫМ АНАЛИЗОМ КОМПОНЕНТОВ**
```
❌ ЗАПРЕЩЕНО: Делать предположения о содержании файлов/компонентов
✅ ОБЯЗАТЕЛЬНО:
@ -184,6 +206,7 @@
```
#### **СТОП-СИГНАЛ #2: ПРИ НЕОПРЕДЕЛЕННОСТИ**
```
❌ ЗАПРЕЩЕНО: Гадать, предполагать, домысливать
✅ ОБЯЗАТЕЛЬНО:
@ -193,6 +216,7 @@
```
#### **СТОП-СИГНАЛ #3: ПЕРЕД ВЫПОЛНЕНИЕМ СРЕДНИХ/СЛОЖНЫХ ЗАДАЧ**
```
❌ ЗАПРЕЩЕНО: Сразу приступать к работе
✅ ОБЯЗАТЕЛЬНО:
@ -204,6 +228,7 @@
### 🔒 СИСТЕМА ПРИНУДИТЕЛЬНЫХ ПРОВЕРОК
#### **ПРОВЕРКА #1: АНАЛИЗ КОДА**
```
Если задача включает анализ компонентов:
□ Использовал ли поиск по кодовой базе?
@ -212,6 +237,7 @@
```
#### **ПРОВЕРКА #2: СОБЛЮДЕНИЕ ПРОТОКОЛОВ**
```
Для каждой задачи:
□ Определил ли сложность задачи?
@ -223,14 +249,17 @@
### ⚡ СИСТЕМА АВТОМАТИЧЕСКИХ ТРИГГЕРОВ
#### **ТРИГГЕР #1: При упоминании компонентов**
- Ключевые слова: "компонент", "файл", "содержание", "показывает"
- Действие: ОБЯЗАТЕЛЬНО использовать инструменты анализа кода
#### **ТРИГГЕР #2: При неопределенности**
- Ключевые фразы: "возможно", "вероятно", "думаю", "предполагаю"
- Действие: СТОП + вопрос пользователю
#### **ТРИГГЕР #3: При работе с UI/UX**
- Ключевые слова: "дизайн", "интерфейс", "компонент", "стили", "UI", "UX", "визуал", "цвет", "кнопка", "форма", "карточка"
- Действие: ОБЯЗАТЕЛЬНО прочитать visual-design-rules.md перед началом работы
@ -250,6 +279,7 @@
### 🛑 ОБЯЗАТЕЛЬНЫЙ ПРОТОКОЛ ПЕРЕД КАЖДОЙ ЗАДАЧЕЙ
#### **ШАГ 1: ОПРЕДЕЛЕНИЕ СЛОЖНОСТИ И ПРОТОКОЛА**
```
ВОПРОСЫ:
- Сколько файлов затрагивает задача? (1-3 = средняя, 4+ = высокая)
@ -260,6 +290,7 @@
```
#### **ШАГ 2: ЭТАП "СТОП И ПОДУМАЙ"**
```
ОБЯЗАТЕЛЬНЫЕ ВОПРОСЫ:
- Какие правила из этого документа применимы?
@ -281,6 +312,7 @@
```
### 📈 МЕТРИКИ УСПЕХА
```
ЦЕЛЬ: 0 пропущенных критических деталей
@ -303,12 +335,13 @@
### 🔍 БЫСТРЫЙ ПОИСК ПО ТЕМАМ
| Тема | Раздел | Ключевые понятия |
|------|--------|------------------|
| ----------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------- |
| **Типы предметов** | [2](#2--типизация-предметов) | PRODUCT, CONSUMABLE, DEFECT, FINISHED_PRODUCT |
| **Кабинет фулфилмента** | [11](#11--кабинет-фулфилмента-полная-документация) | Склад, Услуги, Сотрудники, 6 модулей |
| **Workflow поставок** | [5](#5--workflow-поставок) | 8 статусов, уведомления, логистика |
| **GraphQL запросы** | [18](#18--graphql-и-typescript-правила), [24](#24--технические-приложения) | Резолверы, мутации, типизация |
| **Система партнерства** | [13](#13--система-партнерства-и-контрагентов) | Counterparty, WHOLESALE, заявки |
| **Рынки и маркет** | [10.1](#101-разделение-понятий-рынок-vs-маркет), [18.7](#187-правила-рынков-и-маркета) | РЫНОК ≠ МАРКЕТ, Organization.market |
| **Критические запреты** | [17](#17--критические-запреты) | Что НЕЛЬЗЯ делать в системе |
### 🎯 ДЛЯ РАЗНЫХ РОЛЕЙ
@ -320,15 +353,18 @@
---
## 🔤 ГЛОССАРИЙ ТЕРМИНОВ
> Для людей → `В коде`
### **ТИПЫ ПРЕДМЕТОВ:**
- **ТОВАР** → `PRODUCT` - базовый товар от поставщика, может стать продуктом или браком
- **РАСХОДНИКИ** → `CONSUMABLE` - материалы, классифицируются по назначению при использовании (операционные/производственные)
- **БРАК** → `DEFECT` *(НЕ РЕАЛИЗОВАНО)* - функционал брака еще не внедрен в систему
- **ПРОДУКТ** → `FINISHED_PRODUCT` *(планируется)* - готовый товар, создается из товара по рецептуре
- **БРАК** → `DEFECT` _(НЕ РЕАЛИЗОВАНО)_ - функционал брака еще не внедрен в систему
- **ПРОДУКТ** → `FINISHED_PRODUCT` _(планируется)_ - готовый товар, создается из товара по рецептуре
### **ТИПЫ ОРГАНИЗАЦИЙ:**
- **ПОСТАВЩИК** → `WHOLESALE` - создает товары и расходники, обрабатывает заказы
- **СЕЛЛЕР** → `SELLER` - заказывает товары, создает поставки на маркетплейсы
- **ФУЛФИЛМЕНТ** → `FULFILLMENT` - обрабатывает товары, создает продукты, максимальные права
@ -337,11 +373,13 @@
### 2.2 Правила создания предметов по ролям
**КТО МОЖЕТ СОЗДАВАТЬ:**
- **ПОСТАВЩИК** (`WHOLESALE`): Товары (`PRODUCT`) и Расходники (`CONSUMABLE`)
- **ФУЛФИЛМЕНТ** (`FULFILLMENT`): Продукты (`FINISHED_PRODUCT`) - только из существующих товаров
- **СЕЛЛЕР/ЛОГИСТ**: НЕ МОГУТ создавать предметы
**КТО МОЖЕТ ПОКУПАТЬ:**
- **СЕЛЛЕР** (`SELLER`):
- Товары и расходники у поставщиков
- Расходники фулфилмента у фулфилмента (через рецептуру в поставке)
@ -349,20 +387,73 @@
- **ПОСТАВЩИК/ЛОГИСТ**: НЕ МОГУТ покупать предметы
**ЭКОНОМИЧЕСКИЙ УЧЕТ:**
- Когда селлер выбирает расходники фулфилмента в рецептуре, это формирует экономические данные:
- В кабинете селлера: расход на расходники фулфилмента
- В кабинете фулфилмента: доход от продажи расходников селлеру
### **КЛЮЧЕВЫЕ СУЩНОСТИ:**
- **Контрагент** → `Counterparty` - связь между организациями для партнерства
- **Поставка** → `SupplyOrder` - заказ товаров/расходников с workflow статусами
- **Рецептура** - состав продукта: товар + услуги + расходники (задается селлером)
### **КОНТЕКСТНО-ЗАВИСИМЫЕ ТЕРМИНЫ:**
#### **SupplyOrder - многосторонний документ**
SupplyOrder представляет собой единый документ, который видится по-разному каждым участником процесса:
**ДЛЯ СОЗДАТЕЛЕЙ (Селлер/Фулфилмент):**
- **Термин**: "Поставка"
- **Контекст**: Они создают поставку товаров и расходников на фулфилмент
- **Включает**: Весь процесс от закупки до приемки на склад
**ДЛЯ ПОСТАВЩИКА (исполнитель товарной части):**
- **Термин**: "Заявка на покупку"
- **Контекст**: Получают запрос на продажу своих товаров/расходников
- **Действия**: Могут одобрить или отклонить в зависимости от наличия
**ДЛЯ ЛОГИСТИКИ (исполнитель транспортной части):**
- **Термин**: "Заявка на доставку"
- **Контекст**: Получают запрос на транспортировку груза
- **Действия**: Могут подтвердить или отклонить в зависимости от возможностей
**ОТОБРАЖЕНИЕ В ИНТЕРФЕЙСЕ КАБИНЕТОВ:**
| Кабинет | Название раздела | Обоснование |
|---------|-----------------|-------------|
| Селлер | "Мои поставки" | Создает и управляет поставками |
| Поставщик | "Заявки на покупку" | Обрабатывает входящие заявки |
| Логистика | "Заявки на доставку" | Управляет транспортировкой |
| Фулфилмент | "Входящие поставки" | Принимает поставки на склад |
**ВАЖНО**: Это один и тот же объект SupplyOrder в базе данных, но каждый участник работает со своей стороной процесса.
#### **Маркет vs Маркетплейс - четкое разделение**
**МАРКЕТ** (`/market`):
- **Что это**: Внутренний раздел системы
- **Функция**: Глобальный каталог всех товаров от всех поставщиков
- **Доступ**: Для всех типов организаций в системе
- **НЕ путать**: С названиями физических рынков типа "ОПТ Маркет"
**МАРКЕТПЛЕЙС** (Wildberries, Ozon):
- **Что это**: Внешние торговые площадки
- **Функция**: Конечные точки продаж для селлеров
- **Интеграция**: Через API ключи в настройках
- **Использование**: "Поставки на маркетплейсы", "Отгрузка на маркетплейсы"
---
## 📑 ОГЛАВЛЕНИЕ
> 📋 **ЧТО ОБЪЕДИНЕНО**:
>
> - rules-unified.md (v3.0) - общая база знаний системы
> - fulfillment-cabinet-rules.md (v1.0) - детализация кабинета фулфилмента
> - Устранены все несоответствия в терминах, последовательностях и детализации
@ -426,18 +517,18 @@
### 📦 **ОСНОВНЫЕ ПРЕДМЕТЫ**
| Сущность | Название в системе | Кабинет создания | Описание | Статус |
| ---------- | ---------------------------------- | ---------------- | ----------------------------------------------- | --------------- |
| ---------- | -------------------------------------- | ---------------- | ----------------------------------------------- | -------------- |
| Товар | `Product` (type: `PRODUCT`) | Поставщик | Базовый тип товара от поставщика | ✅ Реализовано |
| Расходники | `Product` (type: `CONSUMABLE`) | Поставщик | Материалы и вспомогательные товары | ✅ Реализовано |
| Брак | `Product` (type: `DEFECT`)* | Фулфилмент | Производная от товара с дефектами | 📋 Планируется |
| Продукт | `Product` (type: `FINISHED_PRODUCT`)* | Фулфилмент | Готовый к продаже товар (производная от товара) | 📋 Планируется |
| Брак | `Product` (type: `DEFECT`)\* | Фулфилмент | Производная от товара с дефектами | 📋 Планируется |
| Продукт | `Product` (type: `FINISHED_PRODUCT`)\* | Фулфилмент | Готовый к продаже товар (производная от товара) | 📋 Планируется |
> **\* Планируется**: Типы `DEFECT` и `FINISHED_PRODUCT` еще не добавлены в Prisma схему
### 🏢 **ОРГАНИЗАЦИИ И РОЛИ**
| Сущность | Название в системе | Основные функции | Статус |
| ---------- | ---------------------------------- | --------------------------------------- | -------------- |
| ---------- | ------------------------------------ | --------------------------------------- | -------------- |
| Поставщик | `Organization` (type: `WHOLESALE`) | Создание товаров, управление поставками | ✅ Реализовано |
| Селлер | `Organization` (type: `SELLER`) | Заказ товаров, управление поставками | ✅ Реализовано |
| Фулфилмент | `Organization` (type: `FULFILLMENT`) | Обработка товаров, управление складом | ✅ Реализовано |
@ -446,7 +537,7 @@
### 🤝 **СИСТЕМА ПАРТНЕРСТВА**
| Сущность | Название в системе | Описание | Статус |
| ------------ | ------------------ | ---------------------------------- | -------------- |
| ---------- | --------------------- | ------------------------- | -------------- |
| Контрагент | `Counterparty` | Связь между организациями | ✅ Реализовано |
| Заявка | `CounterpartyRequest` | Запрос на сотрудничество | ✅ Реализовано |
@ -459,10 +550,12 @@
**СТРУКТУРА СИСТЕМЫ ПО КАБИНЕТАМ:**
**🏢 КАБИНЕТ ПОСТАВЩИКА** - создает и управляет:
- **ТОВАР** (`PRODUCT`) - базовые товары от поставщика
- **РАСХОДНИКИ** (`CONSUMABLE`) - материалы и вспомогательные товары от поставщика
**🏭 КАБИНЕТ ФУЛФИЛМЕНТА** - принимает, обрабатывает и управляет всеми типами:
- **ТОВАР** (`PRODUCT`) - базовые товары от поставщиков (принятые на склад)
- **БРАК** (`DEFECT` - планируется) - производная от товара (товар с дефектами)
- **ПРОДУКТ** (`FINISHED_PRODUCT` - планируется) - готовый к продаже товар
@ -471,6 +564,7 @@
- **"Производственные расходники"** - используются в рецептурах селлеров для создания продуктов
**🛍️ КАБИНЕТ СЕЛЛЕРА** - заказывает и управляет поставками:
- Создает заказы товаров и расходников
- Управляет поставками на фулфилмент и маркетплейсы
- Отслеживает статусы поставок
@ -556,14 +650,17 @@
### 3.2 Специфические разделы по типам организаций
**🏪 ПОСТАВЩИК (`WHOLESALE`):**
- Склад (`/warehouse`) - управление товарами и расходниками
- Поставки (`/supplies`) - обработка заказов от селлеров
**🛍️ СЕЛЛЕР (`SELLER`):**
- Мои поставки (`/supplies`) - управление заказами товаров
- WB Интеграция (`/wb-integration`) - связь с Wildberries
**🏭 ФУЛФИЛМЕНТ (`FULFILLMENT`):**
- Склад фулфилмента (`/fulfillment-warehouse`) - управление всеми типами товаров
- Поставки фулфилмента (`/fulfillment-supplies`) - обработка поставок
- Услуги (`/services`) - управление услугами, логистикой, расходниками
@ -571,6 +668,7 @@
- Статистика фулфилмента (`/fulfillment-statistics`) - детальная аналитика
**🚚 ЛОГИСТИКА (`LOGIST`):**
- Заявки (`/logistics-requests`) - управление заявками на доставку
- Маршруты (`/routes`) - планирование маршрутов
@ -593,15 +691,15 @@
```typescript
const handleSuppliesClick = () => {
switch (user?.organization?.type) {
case "FULFILLMENT":
router.push("/fulfillment-supplies");
break;
case "SELLER":
router.push("/supplies");
break;
case 'FULFILLMENT':
router.push('/fulfillment-supplies')
break
case 'SELLER':
router.push('/supplies')
break
// ... другие типы
}
};
}
```
### 4.2 GraphQL проверки доступа
@ -610,15 +708,15 @@ const handleSuppliesClick = () => {
```typescript
const { data } = useQuery(GET_MY_SERVICES, {
skip: user?.organization?.type !== "FULFILLMENT",
});
skip: user?.organization?.type !== 'FULFILLMENT',
})
```
**В GraphQL резолверах:**
```typescript
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Доступно только для фулфилмент центров");
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
}
```
@ -651,36 +749,25 @@ if (currentUser.organization.type !== "FULFILLMENT") {
### 5.2 Пошаговый процесс поставки
**ЭТАП 1: Создание заказа**
1. Селлер заказывает товар/расходники у поставщика
2. Система создает SupplyOrder со статусом `PENDING`
3. Автоматическое уведомление поставщику
**ЭТАП 2: Обработка поставщиком**
4. Поставщик получает оповещение
5. Поставщик нажимает "Одобрить"
6. Статус меняется на `SUPPLIER_APPROVED`
**ЭТАП 2: Обработка поставщиком** 4. Поставщик получает оповещение 5. Поставщик нажимает "Одобрить" 6. Статус меняется на `SUPPLIER_APPROVED`
**ЭТАП 3: Передача в фулфилмент**
7. Поставка отображается в кабинете фулфилмента
8. Фулфилмент выбирает ответственного и логистику
9. Статус меняется на `CONFIRMED`
**ЭТАП 3: Передача в фулфилмент** 7. Поставка отображается в кабинете фулфилмента 8. Фулфилмент выбирает ответственного и логистику 9. Статус меняется на `CONFIRMED`
**ЭТАП 4: Логистическое подтверждение**
10. Логистика подтверждает доставку
11. Статус меняется на `LOGISTICS_CONFIRMED`
**ЭТАП 4: Логистическое подтверждение** 10. Логистика подтверждает доставку 11. Статус меняется на `LOGISTICS_CONFIRMED`
**ЭТАП 5: Отгрузка**
12. Поставщик отгружает товар
13. Статус меняется на `SHIPPED`, затем `IN_TRANSIT`
**ЭТАП 5: Отгрузка** 12. Поставщик отгружает товар 13. Статус меняется на `SHIPPED`, затем `IN_TRANSIT`
**ЭТАП 6: Доставка и приемка**
14. Логистика доставляет на фулфилмент
15. Фулфилмент принимает товар
16. Статус меняется на `DELIVERED`
**ЭТАП 6: Доставка и приемка** 14. Логистика доставляет на фулфилмент 15. Фулфилмент принимает товар 16. Статус меняется на `DELIVERED`
### 5.3 Система уведомлений
**Обязательные уведомления:**
- Поставщику: о новом заказе
- Фулфилменту: о подтвержденной поставке
- Логистике: о назначении на заявку
@ -691,6 +778,7 @@ if (currentUser.organization.type !== "FULFILLMENT") {
## 6. 🔄 ПРОЦЕСС СОЗДАНИЯ ПРОДУКТА
> 📌 **СВЯЗАННЫЕ РАЗДЕЛЫ**:
>
> - Типы предметов → См. [раздел 2.2](#22-обязательные-поля-карточки)
> - Склад фулфилмента → См. [раздел 11.2](#112-структура-раздела-склад-фулфилмента)
> - Статистика движения → См. [раздел 7](#7--система-учета-движения-товаров)
@ -700,6 +788,7 @@ if (currentUser.organization.type !== "FULFILLMENT") {
> 📌 **ВИЗУАЛЬНЫЕ ПРАВИЛА**: См. [visual-design-rules.md - Процесс создания продукта](#143-процесс-создания-продукта---визуальный-workflow)
#### **ПРЕДВАРИТЕЛЬНОЕ УСЛОВИЕ: РЕЦЕПТУРА ЗАДАНА** (селлер)
```
Время: при создании заявки на поставку
Действие: селлер указывает рецептуру продукта
@ -711,6 +800,7 @@ if (currentUser.organization.type !== "FULFILLMENT") {
```
#### **ШАГ 1: ПОСТУПЛЕНИЕ НА СКЛАД** (автоматически)
```
Время: при смене статуса поставки DELIVERED
Действие: товар переходит в статус "на складе"
@ -719,6 +809,7 @@ if (currentUser.organization.type !== "FULFILLMENT") {
```
#### **ШАГ 2: ПЛАНИРОВАНИЕ РАБОТЫ** (менеджер фулфилмента)
```
Время: в течение 2 рабочих дней после поступления
Действие: назначение параметров обработки
@ -734,6 +825,7 @@ if (currentUser.organization.type !== "FULFILLMENT") {
```
#### **ШАГ 3: ОБРАБОТКА ТОВАРА** (исполнитель)
```
Время: согласно дедлайну (обычно 1-3 дня)
Действие: физическая обработка товара
@ -757,6 +849,7 @@ if (currentUser.organization.type !== "FULFILLMENT") {
```
#### **ШАГ 4: КОНТРОЛЬ КАЧЕСТВА** (менеджер/отдел качества)
```
Время: сразу после завершения ШАГ 3
Действие: приемка готовой продукции
@ -770,6 +863,7 @@ if (currentUser.organization.type !== "FULFILLMENT") {
```
#### **ШАГ 5: ЗАВЕРШЕНИЕ** (система + менеджер)
```
Время: после успешного прохождения контроля качества
Действие: финализация процесса
@ -787,7 +881,7 @@ if (currentUser.organization.type !== "FULFILLMENT") {
### 6.2 Временные рамки и SLA
| Этап | Стандартное время | Максимальное время | Ответственный |
|------|------------------|-------------------|---------------|
| ----------------- | ----------------- | ------------------ | -------------- |
| Планирование | 1 рабочий день | 2 рабочих дня | Менеджер ФФ |
| Обработка | 2-3 рабочих дня | 5 рабочих дней | Исполнитель |
| Контроль качества | 4 часа | 1 рабочий день | Отдел качества |
@ -832,11 +926,13 @@ if (currentUser.organization.type !== "FULFILLMENT") {
**ФАКТ**: Реальное количество после пересчета (работник фулфилмента производит сортировку при пересчете)
**ФИКСАЦИЯ ПОТЕРЬ:**
- **КОГДА**: В процессе работы (вкладка "В работе")
- **ЧТО**: Недостача, повреждения (без создания записей брака)
- **КАК**: Корректировка количества в статистике
**WORKFLOW СОЗДАНИЯ ПРОДУКТА:**
1. Товар поступает на склад фулфилмента (статус "на складе")
2. Товар берется в работу (переход в статус "в обработке")
3. Исполнитель производит пересчет и сортировку
@ -844,10 +940,11 @@ if (currentUser.organization.type !== "FULFILLMENT") {
5. Продукт готов к отправке на маркетплейсы
**ВЛИЯНИЕ НА СТАТИСТИКУ:**
- При принятии поставки: +План в статистику
- При выявлении факта: корректировка на реальные данные
- **ФОРМУЛА**: Факт = Потери + Хороший товар
*Где потери - это недостача/повреждения, выявленные при пересчете и сортировке*
_Где потери - это недостача/повреждения, выявленные при пересчете и сортировке_
- **ЛОГИКА**: Фактическое количество = сумма всех пересчитанных предметов
- **ПЛАН/ФАКТ**: Корректировка статистики при выявлении расхождений
@ -932,6 +1029,7 @@ if (currentUser.organization.type !== "FULFILLMENT") {
- ⚙️ **Настройки** - профиль и конфигурация
**СПЕЦИАЛИЗИРОВАННЫЕ РАЗДЕЛЫ** (зависят от типа кабинета):
- Определяются в соответствующих разделах каждого кабинета
### 8.2 Правила sidebar навигации
@ -963,20 +1061,20 @@ if (currentUser.organization.type !== "FULFILLMENT") {
// Пример: кнопка "Поставки" ведет на разные страницы
const handleSuppliesClick = () => {
switch (user?.organization?.type) {
case "FULFILLMENT":
router.push("/fulfillment-supplies");
break;
case "SELLER":
router.push("/supplies");
break;
case "WHOLESALE":
router.push("/supplies");
break;
case "LOGIST":
router.push("/logistics-orders");
break;
case 'FULFILLMENT':
router.push('/fulfillment-supplies')
break
case 'SELLER':
router.push('/supplies')
break
case 'WHOLESALE':
router.push('/supplies')
break
case 'LOGIST':
router.push('/logistics-orders')
break
}
};
}
```
---
@ -1005,30 +1103,37 @@ const handleSuppliesClick = () => {
**ОБНОВЛЕННАЯ СТРУКТУРА СИСТЕМЫ (4 БЛОКА):**
**БЛОК 1: ПОСТАВЩИКИ** _(горизонтальный скролл)_
- **Отображение**: Карточки поставщиков из раздела "Партнеры"
- **Навигация**: Горизонтальный скролл (слева-направо) при превышении ширины экрана
**БЛОК 1: ПОСТАВЩИКИ** _(адаптивная сетка)_
- **Заголовок**: Минималистичный "🏢 Поставщики" без лишних элементов
- **Поиск**: Компактное поле справа "Поиск поставщиков..." (w-64)
- **Отображение**: Карточки поставщиков из раздела "Партнеры" в адаптивной сетке
- **Выбор**: Клик выделяет карточку поставщика
- **Результат**: Загружаются карточки товаров выбранного поставщика в блок 2
**БЛОК 2: КАРТОЧКИ ТОВАРОВ** _(горизонтальный скролл - НОВЫЙ)_
- **Отображение**: Компактные карточки товаров выбранного поставщика
- **Навигация**: Горизонтальный скролл аналогично блоку 1
- **Отображение**: ТОЛЬКО минималистичные карточки товаров 80×112px
- **Содержание**: ТОЛЬКО изображение товара, БЕЗ текста/названий/цен
- **Навигация**: Горизонтальный скролл при множестве товаров
- **Выбор**: Клик добавляет товар в детальный каталог
- **Результат**: Товар добавляется в блок 3 для управления поставкой
**БЛОК 3: ТОВАРЫ ПОСТАВЩИКА** _(детальный каталог)_
- **Отображение**: Детальные карточки выбранных товаров
- **Управление**: Количество, параметры, настройки поставки
- **Результат**: Формирование окончательной поставки
**БЛОК 4: КОРЗИНА И НАСТРОЙКИ** _(правая панель)_
- **Отображение**: Корзина поставки + настройки
- **Управление**: Фулфилмент-центр, дата, логистика
#### **9.2.1 Детальные правила горизонтального скролла поставщиков**
**СТРУКТУРА И ОТОБРАЖЕНИЕ:**
- **Источник данных**: Партнеры типа `WHOLESALE` из раздела "Партнеры"
- **Контейнер**: Фиксированная высота 176px (h-44) с горизонтальным скроллом
- **Блок поставщиков**: Общая высота 180px, включает заголовок + контейнер скролла
@ -1036,17 +1141,20 @@ const handleSuppliesClick = () => {
- **Поведение**: Плавный скролл с автоскрытием полосы прокрутки
**РАЗМЕРЫ И АДАПТИВНОСТЬ:**
- **Десктоп**: Карточка 216×92px, отступы 12px между карточками, 16px от краев
- **Планшет**: Карточка 200×92px, отступы 12px между карточками
- **Мобильный**: Карточка 184×92px, отступы 12px между карточками
- **Высота блока**: 180px фиксированная для всего блока поставщиков
**ВЗАИМОДЕЙСТВИЕ:**
- **Навигация**: Колесо мыши (Shift+скролл), стрелки клавиатуры, свайп на тач
- **Выбор**: Клик по карточке → активная рамка + загрузка товаров в блок 2
- **Состояния**: Default, Hover (box-shadow), Active (цветная рамка), Loading (скелетон)
**ГРАНИЧНЫЕ СЛУЧАИ:**
- **1-4 карточки**: Выравнивание по левому краю, скролл неактивен
- **5+ карточек**: Полный горизонтальный скролл
- **Нет партнеров**: Заглушка с ссылкой на раздел "Партнеры"
@ -1054,6 +1162,7 @@ const handleSuppliesClick = () => {
**ТЕХНИЧЕСКАЯ РЕАЛИЗАЦИЯ:**
**Критическая Flex-архитектура:**
```css
.parent-container {
display: flex;
@ -1081,6 +1190,7 @@ const handleSuppliesClick = () => {
```
**Контейнер скролла:**
```css
.suppliers-block {
display: flex;
@ -1109,22 +1219,26 @@ const handleSuppliesClick = () => {
**СОДЕРЖАНИЕ КАРТОЧКИ ПОСТАВЩИКА:**
**Структура (3 строки в 92px высоты):**
- **Строка 1**: Название + рейтинг (справа, если есть)
- **Строка 2**: ИНН (формат "ИНН: 1234567890")
- **Строка 3**: Бейдж рынка (отдельная строка)
**Элементы:**
- **Аватар**: Размер xs, слева с gap-2
- **Текст**: text-xs для компактности
- **Отступы**: mb-1 между строками 1-2, mb-0.5 между строками 2-3
- **Padding карточки**: 8px (p-2)
**ЦВЕТОВАЯ СХЕМА РЫНКОВ:**
- **"Садовод"** (sadovod): Зеленый `bg-green-500/20 text-green-300 border-green-500/30`
- **"ТЯК Москва"** (tyak-moscow): Синий `bg-blue-500/20 text-blue-300 border-blue-500/30`
- **Другие/не указан**: Серый `bg-gray-500/20 text-gray-300 border-gray-500/30`
**ДОСТУПНОСТЬ:**
- `role="tablist"` для контейнера
- `role="tab"` для карточек
- `aria-selected="true/false"` для выбранной карточки
@ -1133,31 +1247,341 @@ const handleSuppliesClick = () => {
#### **9.2.2 Правила блока "Карточки товаров" (Блок 2)**
**НАЗНАЧЕНИЕ И ЛОГИКА:**
- **Источник данных**: Товары выбранного поставщика из Блока 1
- **Триггер отображения**: Клик на карточку поставщика → загрузка карточек товаров
- **Взаимодействие**: Клик на карточку товара → добавление в Блок 3 "Товары поставщика"
- **Поведение**: Горизонтальный скролл при множестве товаров (аналогично Блоку 1)
- **Поведение**: Горизонтальный скролл при множестве товаров
**АРХИТЕКТУРА И РАЗМЕРЫ:**
- **Общая высота блока**: 160px фиксированная
- **Заголовок**: "Товары [Название поставщика]" + поиск (~40px)
- **Контейнер скролла**: 120px (h-30) с горизонтальным скроллом
- **Внешний контейнер**: bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-shrink-0
- **Внутренний контейнер скролла**: flex gap-3 overflow-x-auto p-4
- **Стилизация скролла**: scrollbarWidth: 'thin' для тонкой полосы прокрутки
- **Отступы**: padding: 16px (p-4) внутри, gap: 12px (gap-3) между карточками
- **Адаптивная высота**: по содержимому карточек (БЕЗ фиксированной высоты)
- **Визуальное единство**: стеклянный эффект как у других блоков системы
- **БЕЗ заголовков/иконок**: только чистые карточки товаров в контейнере
**РАЗМЕРЫ КАРТОЧЕК ТОВАРОВ:**
- **Компактная карточка**: 80×112px (соотношение 5:7), вертикальное изображение
- **Отступы**: 12px между карточками, без дополнительных отступов от краев
- **Компактная карточка**: 80×112px (w-20 h-28), соотношение 5:7
- **Адаптивность**: фиксированный размер для всех устройств
**СОДЕРЖАНИЕ КАРТОЧКИ ТОВАРА:**
- **Только изображение**: 80×112px товара, вертикальное
- **Минималистичный дизайн**: без текста, названий, цен
- **Состояния**: выбранное/невыбранное с визуальной индикацией
- **Hover эффект**: увеличение border, изменение тени
- **ТОЛЬКО изображение товара**: 80×112px, object-cover
- **Минималистичный дизайн**: БЕЗ текста, названий, цен, иконок
- **Состояния**: Default, Selected, Active (БЕЗ Hover-эффектов)
- **Рамка**: border-white/10, при выборе border-white/30
- **Фон**: bg-white/5 полупрозрачный
**ДЕЙСТВИЕ:**
Клик на карточку → добавление товара в Блок 3 (детальный каталог)
### 9.2.2.1 ПРАВИЛО ПЕРСИСТЕНТНОСТИ ВЫБРАННЫХ ТОВАРОВ
#### **9.2.3 Правила Блока 3 "Детальный каталог товаров"**
**НАЗНАЧЕНИЕ И СТРУКТУРА:**
- **Контент**: Детальные карточки выбранных товаров с полным управлением
- **Верхняя панель**: Выбор даты + Выбор Fulfillment + Поиск
- **Основная область**: Сетка карточек товаров с детальной информацией
#### **9.2.3.1 Структура верхней панели Блока 3**
**МИНИМАЛИСТИЧНАЯ ПАНЕЛЬ УПРАВЛЕНИЯ:**
- **Выбор даты поставки**: DatePicker для планирования поставки
- **Выбор Fulfillment-центра**: Select dropdown со списком доступных фулфилментов
- **Поиск по товарам**: Input с иконкой поиска и placeholder
- **Компоновка**: Горизонтальная строка с равномерным распределением
**ТЕХНИЧЕСКИЕ ТРЕБОВАНИЯ:**
```tsx
// Структура компонентов панели
<div className="flex items-center gap-4 p-4 bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl mb-4">
<DatePicker placeholder="Дата поставки" />
<Select placeholder="Выберите фулфилмент">
<SelectContent>
{fulfillmentCenters.map((center) => (
<SelectItem value={center.id}>{center.name}</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
<Input placeholder="Поиск товаров..." className="pl-10 glass-input" />
</div>
</div>
```
#### **9.2.3.2 Структура основной области карточек**
**СЕТКА ТОВАРОВ:**
- **Адаптивная сетка**: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`
- **Детальные карточки**: Полная информация + количество + управление
- **Состояния**: Default, Selected, Editing
- **Интерактивность**: Изменение количества, удаление, настройки рецептуры
**ФУНКЦИОНАЛЬНОСТЬ ПАНЕЛИ:**
- **Выбор даты**: Планирование времени поставки (обязательное поле)
- **Выбор фулфилмента**: Определение исполнителя поставки (обязательное поле)
- **Поиск**: Фильтрация товаров в каталоге по названию/артикулу
- **Валидация**: Блокировка создания поставки без заполнения даты и фулфилмента
**ГРАНИЧНЫЕ СЛУЧАИ:**
- **Пустой каталог**: Заглушка "Добавьте товары"
- **Нет фулфилментов**: Сообщение "Настройте партнерство с фулфилмент-центрами"
- **Поиск без результатов**: "По запросу ничего не найдено"
#### **9.2.2.1 Структура контейнера Блока 2**
**ДВУХУРОВНЕВАЯ АРХИТЕКТУРА:**
**УРОВЕНЬ 1 - Внешний контейнер (блок):**
```jsx
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-shrink-0">
```
- **Назначение**: Визуальное обрамление блока, единство с другими блоками
- **Стилизация**: Стеклянный эффект с размытием и полупрозрачностью
- **Рамка**: Тонкая белая рамка border-white/20 с закруглёнными углами
- **Поведение**: flex-shrink-0 предотвращает сжатие блока
**УРОВЕНЬ 2 - Внутренний контейнер (скролл):**
```jsx
<div className="flex gap-3 overflow-x-auto p-4" style={{ scrollbarWidth: 'thin' }}>
```
- **Назначение**: Горизонтальная прокрутка карточек товаров
- **Раскладка**: Flex с промежутками gap-3 (12px) между карточками
- **Отступы**: padding p-4 (16px) со всех сторон
- **Скролл**: overflow-x-auto с тонкой полосой прокрутки
- **Поведение**: Автоматическое появление скролла при превышении ширины
**ПРАВИЛА КОНТЕЙНЕРОВ:**
- Внешний контейнер НЕ содержит заголовков, иконок, описаний
- Внутренний контейнер содержит ТОЛЬКО карточки товаров
- Высота адаптируется под размер карточек (80×112px + отступы)
- Визуальное единство со всеми блоками формы поставки
**ТЕХНИЧЕСКИЕ ПРАВИЛА:**
- **Условие отображения**: selectedSupplier && products.length > 0
- **Источник данных**: products массив из GraphQL запроса organizationProducts
- **Реактивность**: Автоматическое обновление при смене поставщика
- **Производительность**: React.memo для карточек при большом количестве товаров
- **Доступность**: Клавиатурная навигация (Tab, Enter для выбора)
**UX ПРАВИЛА ВЗАИМОДЕЙСТВИЯ:**
- **Скролл**: Автоматическое появление при превышении ширины контейнера
- **Индикация загрузки**: Скелетоны карточек во время загрузки товаров
- **Пустое состояние**: Скрытие блока при отсутствии поставщика или товаров
- **Фокус**: Первая карточка получает фокус при загрузке товаров
- **Навигация**: Стрелки ←→ для перемещения между карточками
**СОСТОЯНИЯ БЛОКА:**
- **Скрыт**: При отсутствии выбранного поставщика
- **Скрыт**: При отсутствии товаров у поставщика
- **Активен**: При наличии поставщика и товаров
- **Загрузка**: Показ скелетонов карточек во время запроса
**ПРАВИЛА ПРОИЗВОДИТЕЛЬНОСТИ:**
- **Виртуализация**: При количестве товаров > 100
- **Ленивая загрузка изображений**: loading="lazy" для всех изображений
- **Мемоизация**: React.memo для компонентов карточек
- **Дебаунс**: 300мс для поисковых запросов (если будет добавлен поиск)
**ПРАВИЛА АДАПТИВНОСТИ:**
- **Мобильные устройства**: Свайп для горизонтальной прокрутки
- **Планшеты**: Сохранение размеров карточек 80×112px
- **Десктоп**: Полная функциональность с клавиатурной навигацией
- **Высокие разрешения**: Сохранение пропорций и читаемости
**ПРАВИЛА БЕЗОПАСНОСТИ И ВАЛИДАЦИИ:**
- **Валидация данных**: Проверка существования product.id перед добавлением
- **Дубликаты**: Предотвращение добавления одного товара дважды в детальный каталог
- **Санитизация**: Безопасное отображение названий товаров (XSS защита)
- **Обработка ошибок**: Graceful degradation при ошибках загрузки изображений
- **Защита от спама**: Дебаунс кликов 200мс для предотвращения множественных добавлений
**ПРАВИЛА ИНТЕГРАЦИИ С ДРУГИМИ БЛОКАМИ:**
- **Блок 1 (Поставщики)**: Слушает изменения selectedSupplier для обновления товаров
- **Блок 3 (Детальный каталог)**: Передаёт выбранные товары через setAllSelectedProducts
- **Блок 4 (Корзина)**: Товары добавляются в корзину из Блока 3, не напрямую из Блока 2
- **Синхронизация состояний**: Реактивное обновление при изменении данных в любом блоке
**ПРАВИЛА АНАЛИТИКИ И МЕТРИК:**
- **Отслеживание кликов**: Логирование добавления товаров в детальный каталог
- **Метрики производительности**: Время загрузки товаров поставщика
- **Пользовательское поведение**: Количество просмотренных товаров на поставщика
- **A/B тестирование**: Готовность к тестированию различных размеров карточек
**ПРАВИЛА ЛОКАЛИЗАЦИИ:**
- **Alt-текст изображений**: На языке интерфейса пользователя
- **Направление скролла**: RTL поддержка для арабского/иврита
- **Размеры карточек**: Неизменны для всех локалей (80×112px)
- **Сообщения об ошибках**: Локализованные уведомления при проблемах загрузки
#### **9.2.1.1 Заголовок и поиск Блока 1**
**МИНИМАЛИСТИЧНЫЙ ДИЗАЙН:**
```jsx
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-blue-400" />
<h2 className="text-lg font-semibold text-white">Поставщики</h2>
</div>
<div className="w-64">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-4 w-4" />
<Input
placeholder="Поиск поставщиков..."
className="bg-white/5 border-white/10 text-white placeholder:text-white/50 pl-10 h-9"
/>
</div>
</div>
</div>
```
**ПРАВИЛА ЗАГОЛОВКА:**
- **Иконка**: Building2 h-5 w-5 text-blue-400 (без фонового контейнера)
- **Текст**: "Поставщики" (убран избыточный "товаров")
- **Размер**: text-lg font-semibold (увеличен для лучшей читаемости)
- **БЕЗ бэджа**: Убран избыточный бэдж "Создание поставки"
- **Выравнивание**: flex items-center gap-2 (компактное)
**ПРАВИЛА ПОИСКА:**
- **Позиция**: Справа от заголовка (justify-between)
- **Ширина**: w-64 (256px) фиксированная ширина
- **Плейсхолдер**: "Поиск поставщиков..." (конкретное описание)
- **Иконка**: Search h-4 w-4 слева в поле
- **Стили**: Стандартные glass-эффекты, focus:border-white/20
**ПРАВИЛА КНОПКИ "НАЙТИ В МАРКЕТЕ":**
- **Условие**: Показывается только при allCounterparties.length === 0
- **Позиция**: Отдельный блок под заголовком (mt-4)
- **НЕ интегрирована**: В поле поиска (отдельно)
- **Стили**: glass-secondary outline button размера sm
#### **9.2.1.2 Структура карточки поставщика в Блоке 1**
**МИНИМАЛИСТИЧНАЯ КАРТОЧКА ПОСТАВЩИКА:**
**СТРУКТУРА ИНФОРМАЦИИ:**
```jsx
<div className="flex items-start gap-2">
<OrganizationAvatar organization={supplier} size="sm" />
<div className="flex-1 min-w-0">
<h4 className="text-white font-medium text-sm truncate">{supplier.name || supplier.fullName}</h4>
<div className="flex items-center gap-2 mt-1">
<p className="text-white/60 text-xs font-mono">ИНН: {supplier.inn}</p>
{supplier.market && <Badge className="market-badge">{getMarketLabel(supplier.market)}</Badge>}
</div>
</div>
</div>
```
**ПРАВИЛА СОДЕРЖАНИЯ КАРТОЧКИ:**
**✅ ОСТАВИТЬ:**
- **Аватар организации**: OrganizationAvatar size="sm" слева
- **Название поставщика**: supplier.name || supplier.fullName (приоритет name)
- **ИНН**: font-mono, text-white/60, с префиксом "ИНН: "
**🔸 ДОБАВИТЬ:**
- **Принадлежность к рынку**: Badge с названием рынка из supplier.market
- **Рынки**: "Садовод", "ТЯК Москва" и другие из Organization.market поля
**❌ УБРАТЬ:**
- **Рейтинг**: Звездочка и цифра rating (избыточно)
- **Тип бэдж**: "Поставщик" badge (и так понятно из контекста)
- **Адрес**: supplier.address (занимает место, не критично)
**СТИЛИ РЫНОЧНЫХ БЭДЖЕЙ:**
- **Садовод**: bg-green-500/20 text-green-300 border-green-500/30
- **ТЯК Москва**: bg-blue-500/20 text-blue-300 border-blue-500/30
- **По умолчанию**: bg-gray-500/20 text-gray-300 border-gray-500/30
**ПРАВИЛА АДАПТИВНОСТИ:**
- **Мобильные**: Сохранение структуры, truncate для длинных названий
- **Планшеты/десктоп**: Полное отображение в сетке
- **Малые экраны**: line-clamp-1 для названия организации
**СОСТОЯНИЯ КАРТОЧКИ:**
- **Default**: bg-white/5 border-white/10
- **Hover**: hover:border-white/20 hover:bg-white/10
- **Selected**: bg-white/15 border-white/40 shadow-lg
- **Disabled**: opacity-50 cursor-not-allowed (при недоступности)
**ПРАВИЛА ИНТЕГРАЦИИ С РЫНКАМИ:**
**ИСТОЧНИК ДАННЫХ:**
- **Поле БД**: Organization.market (String?) - поле принадлежности к рынку
- **Настройка**: Указывается в настройках кабинета поставщика
- **Опциональность**: Поле может быть пустым (рынок не указан)
**ФУНКЦИЯ getMarketLabel():**
```jsx
const getMarketLabel = (market?: string) => {
const marketLabels = {
'sadovod': 'Садовод',
'tyak-moscow': 'ТЯК Москва',
'opt-market': 'ОПТ Маркет',
}
return marketLabels[market as keyof typeof marketLabels] || market
}
```
**СТИЛИ ДЛЯ РЫНКОВ:**
```jsx
const getMarketBadgeStyle = (market?: string) => {
const styles = {
'sadovod': 'bg-green-500/20 text-green-300 border-green-500/30',
'tyak-moscow': 'bg-blue-500/20 text-blue-300 border-blue-500/30',
'opt-market': 'bg-purple-500/20 text-purple-300 border-purple-500/30',
}
return styles[market as keyof typeof styles] || 'bg-gray-500/20 text-gray-300 border-gray-500/30'
}
```
**ПРАВИЛА ОТОБРАЖЕНИЯ:**
- **Условие**: Показывать badge только если supplier.market существует
- **Размер**: text-xs для соответствия ИНН
- **Позиция**: Справа от ИНН в той же строке
- **Приоритет**: Рынок важнее типа организации для селлера
### 9.2.2.2 ПРАВИЛО ПЕРСИСТЕНТНОСТИ ВЫБРАННЫХ ТОВАРОВ
**🎯 ОСНОВНОЙ ПРИНЦИП:**
Выбранные товары в детальном каталоге (блок 3) сохраняются при смене поставщика и могут быть удалены только явным действием пользователя.
@ -1165,6 +1589,7 @@ const handleSuppliesClick = () => {
**🔄 WORKFLOW СЦЕНАРИИ:**
**СЦЕНАРИЙ 1: Добавление товаров от разных поставщиков**
1. Пользователь выбирает Поставщика А
2. Добавляет Товар 1 и Товар 2 в детальный каталог
3. Переключается на Поставщика Б
@ -1173,126 +1598,52 @@ const handleSuppliesClick = () => {
6. В блоке 3: Товар 1, Товар 2 (от А) + Товар 3 (от Б)
**СЦЕНАРИЙ 2: Визуальная индикация в блоке 2**
- При переключении на поставщика, товары которого уже есть в блоке 3, показываются как "выбранные"
- Товары от других поставщиков в блоке 2 не отображаются
**🛠️ ТЕХНИЧЕСКИЕ ПРАВИЛА:**
**Состояние selectedProductsForDetailView:**
- Глобальное состояние всех выбранных товаров
- НЕ зависит от текущего поставщика
- НЕ очищается при смене поставщика
- Очищается только явными действиями пользователя
**Единственные способы удаления:**
1. Кнопка "Удалить из каталога" в карточке товара (блок 3)
2. Кнопка "Очистить каталог" в заголовке блока 3
3. НЕ при смене поставщика
**🎨 UX ПРАВИЛА:**
- Счетчик товаров: "Детальный каталог (X товаров от Y поставщиков)"
- Визуальная индикация выбранных товаров в блоке 2
- Информация о поставщике для каждого товара в блоке 3
### 9.2.2.2 ПРАВИЛО ВСПЛЫВАЮЩЕЙ ПОДСКАЗКИ ТОВАРА
**🎯 ОСНОВНОЙ ПРИНЦИП:**
При наведении курсора на компактную карточку товара в блоке 2 появляется динамическое модальное окно с полной информацией о товаре.
**📱 АДАПТИВНОСТЬ:**
- Планшеты: показывать по долгому нажатию (500ms), работает как desktop
- Desktop: стандартное поведение hover (300ms задержка)
- Мобильные: не показывать (< 768px)
**🎨 ДИЗАЙН И РАЗМЕРЫ:**
- Ширина: 220px фиксированная
- Высота: фиксированная (не изменяется)
- Фон: `bg-white/10 backdrop-blur-xl` (подстраивается под блок)
- Граница: `border border-white/20`, скругления: `rounded-xl`
- Тень: `shadow-2xl`
**📊 СТРУКТУРА КОНТЕНТА (ПРИОРИТЕТ):**
**ЗАГОЛОВОК (КРУПНО):**
- Название товара: `text-lg font-semibold`, truncate
- Цена: `text-lg font-bold` "₽ 2,500 за шт"
**ГРУППЫ ИНФОРМАЦИИ:**
1. **ОСНОВНОЕ**: Остатки (с цветовой индикацией) + Категория (badge)
2. **ХАРАКТЕРИСТИКИ** (если есть): Цвет, размеры, объемы, комплектность
3. **СЛУЖЕБНОЕ**: Артикул формата `SP-ABC123456`
**📊 ИСТОЧНИК ДАННЫХ:**
- Только из карточки товара `GoodsProduct` - никаких дополнительных запросов
- Если данных нет - не показываем этот пункт
- Показываем только то, что есть
**🖱 ИНТЕРАКТИВНОСТЬ:**
- Read-only - никакого взаимодействия внутри подсказки
- Клик на карточку добавляет товар (как обычно)
- Подсказка не блокирует основное взаимодействие
**📐 ПОЗИЦИОНИРОВАНИЕ:**
- Умное позиционирование по наибольшему свободному месту
- Приоритет: справа слева сверху снизу
- Частично видимые карточки: все равно показывать подсказку
- Отступы от краев экрана: минимум 16px
**🚨 ОБРАБОТКА ОШИБОК:**
- При ошибках загрузки: не показывать подсказку вообще
- Без изображения: показывать данные как обычно
- Длинные названия: truncate, размеры модалки НЕ изменять
** ПРОИЗВОДИТЕЛЬНОСТЬ:**
- Debounce: 300ms задержка перед показом
- Throttle: позиционирование при скролле/ресайзе
- React.memo для оптимизации рендера
**ТЕХНИЧЕСКАЯ РЕАЛИЗАЦИЯ:**
```css
.products-cards-container {
height: 160px; /* Общая высота блока */
flex-shrink: 0;
min-width: 0; /* Предотвращает растяжение */
}
.products-cards-block {
display: flex;
overflow-x: auto;
scroll-behavior: smooth;
gap: 10px;
padding: 0 12px 6px 12px; /* px-3 pb-1.5 */
height: 120px; /* h-30 */
scrollbar-width: thin;
scrollbar-color: #64748b33 transparent;
}
.product-card {
flex-shrink: 0;
width: 180px; /* Десктоп */
height: 88px; /* Фиксированная высота */
padding: 6px; /* p-1.5 */
transition: all 0.2s ease;
}
```
**СОСТОЯНИЯ БЛОКА:**
- **Не выбран поставщик**: Заглушка "Выберите поставщика для просмотра товаров"
- **Поставщик выбран, нет товаров**: "У поставщика нет товаров"
- **Поставщик выбран, есть товары**: Карточки товаров с горизонтальным скроллом
**ВЗАИМОДЕЙСТВИЕ:**
- **Навигация**: Горизонтальная прокрутка мышью, клавишами ←→
- **Выбор**: Клик → добавление в Блок 3 с анимацией
- **Состояния карточек**: Default, Hover, Active (при добавлении)
- **Состояния карточек**: Default, Selected, Active (при добавлении)
**ГРАНИЧНЫЕ СЛУЧАИ:**
- **1-5 карточек**: Скролл неактивен, выравнивание по левому краю
- **6+ карточек**: Полноценный горизонтальный скролл
- **Поиск**: Фильтрация карточек в реальном времени
- **Загрузка**: Скелетон-анимация при смене поставщика
**БЛОК 3: ТОВАРЫ ПОСТАВЩИКА** _(детальный каталог)_
- **Содержание**: Детальный каталог товаров для управления поставкой
- **Источник**: Товары, добавленные из Блока 2 "Карточки товаров"
- **Сортировка**: По цене, названию, категории
@ -1305,6 +1656,7 @@ const handleSuppliesClick = () => {
- **Действие**: Клик добавляет расходник в корзину
**БЛОК 3: КОРЗИНА** _(правая часть)_
- **Содержание корзины**:
- Количество видов расходников
- По каждому расходнику: название, количество, цена за единицу, сумма
@ -1321,6 +1673,7 @@ const handleSuppliesClick = () => {
#### **📊 Структура многоуровневой таблицы:**
**ПЕРВЫЙ УРОВЕНЬ** _(основной список)_:
- Порядковый номер поставки (от большего к меньшему)
- Количество видов расходников селлера
- Стоимость всей поставки
@ -1329,6 +1682,7 @@ const handleSuppliesClick = () => {
- Кнопка раскрытия/сворачивания
**ВТОРОЙ УРОВЕНЬ** _(раскрывается по клику)_:
- Название расходника селлера
- Количество
- Цена за единицу
@ -1338,6 +1692,7 @@ const handleSuppliesClick = () => {
- **Режим**: Только просмотр (редактирование недоступно после создания)
**ПРАВИЛА ОТОБРАЖЕНИЯ**:
- По умолчанию таблица свернута, показан только первый уровень
- Клик по строке или кнопке раскрывает второй уровень
- Анимация раскрытия плавная (300ms)
@ -1448,18 +1803,21 @@ transition-all duration-150
**ПРАВИЛО**: Статистика меняется в зависимости от выбранных табов
**Для путей "Фулфилмент → Товар → Карточки/Поставщики":**
- Всего поставок
- Активных поставок
- Сумма активных поставок
- В пути
**Для пути "Фулфилмент → Расходники селлера":**
- Всего поставок
- Активных поставок
- Видов расходников
- Критические остатки
**Для путей "Маркетплейсы → Wildberries/Ozon":**
- Поставок на маркетплейс
- Товаров отправлено
- Возвраты за неделю
@ -1468,11 +1826,13 @@ transition-all duration-150
#### **9.3.3 Высота основного блока**
**ФОРМУЛА РАСЧЕТА**:
```css
height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins);
```
**ПРАВИЛО ВЫРАВНИВАНИЯ**:
- Нижняя граница основного блока должна быть на одном уровне с нижней границей sidebar
- При изменении размера окна высота пересчитывается
- Внутренний скролл: `overflow-y-auto`
@ -1480,6 +1840,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### **9.3.4 Сохранение функционала**
**КРИТИЧЕСКИ ВАЖНО**: При добавлении блока статистики весь существующий функционал сохраняется:
- Таблицы с данными поставок
- Фильтры и сортировка
- Кнопки действий
@ -1488,6 +1849,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
- Поиск
**ЗАПРЕЩЕНО**:
- Удалять существующие компоненты
- Изменять логику работы таблиц
- Нарушать существующие API вызовы
@ -1499,6 +1861,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
**КЛЮЧЕВОЕ ПРАВИЛО**: Табы "Карточки" и "Поставщики" - это два способа создания поставок одного типа предмета (ТОВАР)
**СПОСОБЫ СОЗДАНИЯ**:
- **Карточки** - импорт товаров через WB API с автоматическим созданием поставки
- **Поставщики** - прямой заказ товаров у поставщика с указанием рецептуры
@ -1509,6 +1872,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
**ПРАВИЛО**: Блок статистики показывает ОДИНАКОВЫЕ данные для обоих табов
**МЕТРИКИ ДЛЯ ТАБОВ "КАРТОЧКИ" И "ПОСТАВЩИКИ"**:
- Всего поставок товаров (из всех источников)
- Активных поставок товаров (в работе)
- Сумма активных поставок товаров
@ -1521,11 +1885,13 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
**СОДЕРЖИМОЕ**: Единая таблица всех поставок товаров
**ИСТОЧНИКИ ДАННЫХ**:
- Поставки, созданные через импорт карточек WB
- Поставки, созданные через заказ у поставщиков
- Все промежуточные и завершённые поставки
**РАЗЛИЧИЯ ТАБОВ**:
- Только кнопки создания ведут на разные страницы
- Таб "Карточки": `/supplies/create-cards`
- Таб "Поставщики": `/supplies/create-suppliers`
@ -1535,20 +1901,25 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### **Структура страницы**:
**БЛОК 1: ПОСТАВЩИКИ** _(обязательный, 180px)_:
- Карточки поставщиков из раздела "Партнеры"
- Горизонтальный скролл при превышении ширины
- Выбор только одного поставщика одновременно
**БЛОК 2: КАРТОЧКИ ТОВАРОВ** _(160px - НОВЫЙ БЛОК)_:
- Компактные карточки товаров выбранного поставщика
- Горизонтальный скролл аналогично блоку 1
**БЛОК 2: КАРТОЧКИ ТОВАРОВ** _(адаптивная высота - НОВЫЙ БЛОК)_:
- ТОЛЬКО минималистичные карточки товаров 80×112px
- ТОЛЬКО изображение товара, БЕЗ текста/названий/цен
- Горизонтальный скролл при множестве товаров
- Клик добавляет товар в блок 3
**БЛОК 3: ТОВАРЫ ПОСТАВЩИКА** _(flex-1, детальный каталог)_:
- Детальные карточки выбранных товаров
- Управление количеством и параметрами поставки
**БЛОК 4: КОРЗИНА И НАСТРОЙКИ** _(правая панель, 384px)_:
- Корзина поставки с выбранными товарами
- Настройки поставки (фулфилмент-центр, дата, логистика)
- Сортировка: цена, название, категория
@ -1596,14 +1967,69 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
## 10. 🏪 КАБИНЕТ ПОСТАВЩИКА
### 10.1 Основные возможности
### 10.1 Разделение понятий: РЫНОК vs МАРКЕТ
**🔍 КРИТИЧЕСКОЕ РАЗДЕЛЕНИЕ ПОНЯТИЙ:**
### **РЫНОК** 🏪 - физическое торговое место
- **Назначение**: Географическая принадлежность поставщиков
- **Примеры**: Садовод, ТЯК Москва
- **Структура**: Название + адрес
- **Связь**: Поставщик принадлежит рынку
### **МАРКЕТ** 🛒 - раздел системы для торговли
- **Назначение**: Глобальный каталог товаров в системе
- **Роут**: `/market` - просмотр и заказ товаров
- **Содержание**: Все доступные товары от всех поставщиков
- **Связь**: НЕ связан с физическими рынками
**🏢 АРХИТЕКТУРА ПРИНАДЛЕЖНОСТИ:**
```
РЫНОК (физическое место)
└── Поставщик (Organization.market)
└── Товары/Расходники (наследуют рынок от поставщика)
└── Отображаются в МАРКЕТЕ (/market)
```
**🎯 ПРИНЦИПЫ ИЕРАРХИИ:**
1. **РЫНОК → ПОСТАВЩИК**: Поставщик работает на конкретном рынке
2. **ПОСТАВЩИК → ТОВАРЫ**: Товары принадлежат поставщику с его рынка
3. **ТОВАРЫ → МАРКЕТ**: Все товары показываются в глобальном маркете (/market)
4. **НАСЛЕДОВАНИЕ**: Товары получают рынок от организации поставщика
**🏪 ФИЗИЧЕСКИЕ РЫНКИ В СИСТЕМЕ:**
- **"Садовод"** (`sadovod`) - Москва, 14-й км МКАД
- **Цветовая схема**: `bg-green-500/20 text-green-300 border-green-500/30`
- **"ТЯК Москва"** (`tyak-moscow`) - Москва, Алтуфьевское шоссе, 27
- **Цветовая схема**: `bg-blue-500/20 text-blue-300 border-blue-500/30`
**🛒 МАРКЕТ В СИСТЕМЕ:**
- **Роут**: `/market` - глобальный каталог товаров
- **Функции**: Просмотр, поиск, фильтрация, заказ товаров
- **Источник**: Товары от всех поставщиков всех рынков
- **Отображение рынка**: В карточках поставщиков и товаров
**🔧 ТЕХНИЧЕСКАЯ РЕАЛИЗАЦИЯ:**
- **Поле рынка**: `Organization.market` (String?) - принадлежность поставщика к рынку
- **Настройка рынка**: В настройках организации поставщика
- **Отображение в маркете**: Товары показывают рынок через `product.organization.market`
- **Фильтрация**: В маркете по рынку поставщика
### 10.2 Основные возможности
**СОЗДАНИЕ КАРТОЧЕК**:
- **ТОВАР** - базовые товары поставщика
- **РАСХОДНИКИ** - материалы и вспомогательные товары
### 10.2 Обязательные поля карточки
### 10.3 Обязательные поля карточки
**Базовые параметры**:
@ -1618,7 +2044,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
- Цена за единицу и за комплект
- Заказано, В пути, Остаток, Продано
### 10.3 Отображение информации в карточках
### 10.4 Отображение информации в карточках
**Каждая карточка содержит**:
@ -1629,7 +2055,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
- Данные о движении: остаток, заказано, в пути, продано
- Индикаторы низких остатков
### 10.4 Статистика поставщика
### 10.5 Статистика поставщика
**Блок статистики включает**:
@ -1676,10 +2102,12 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### 11.2.2 Система учета склада
**Дополнительные значения** (показатели движения):
- **ПРИБЫЛО** - количество поступивших на склад за период
- **УБЫЛО** - количество списанных со склада за период
**Основные значения** (текущие остатки):
- **ФОРМУЛА**: Основные значения = Предыдущие остатки + Прибыло - Убыло
- **ОБНОВЛЕНИЕ**: В реальном времени с изменениями за сутки
- **ИСТОЧНИК**: GraphQL query `GET_FULFILLMENT_WAREHOUSE_STATS`
@ -1698,6 +2126,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
```
**Цветовое кодирование**:
- Каждый уровень имеет цветной индикатор увеличивающегося размера
- Цветная левая граница с увеличивающимся отступом и толщиной
- Скроллбары в цвете уровня
@ -1731,11 +2160,13 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### 11.5.1 Структура: 2 основные вкладки
**A) 🛒 ПОСТАВКИ ТОВАРОВ**:
- **Детализированные товары ФФ** - планы и факты поставок с маршрутами
- **Товары ФФ** - общие поставки товаров от селлеров
- **Возвраты с ПВЗ** - обработка возвращенных товаров
**B) 🔧 ПОСТАВКИ РАСХОДНИКОВ**:
- **Заказы расходников** - управление заказами от селлеров
- **Расходники селлеров** - материалы для клиентов
- **Создание поставок** - формирование новых поставок расходников
@ -1743,6 +2174,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### 11.5.2 Workflow поставок товаров
**Этапы обработки**:
1. **Planned** - поставка запланирована
2. **In-transit** - товар в пути
3. **Delivered** - доставлен на склад
@ -1753,6 +2185,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### 11.6.1 Блоки аналитики (сворачиваемые)
**1. НАКОПЛЕННАЯ СТАТИСТИКА** (`allTime: true`):
- Обработано товаров (общий объем)
- Выявлено брака (всего единиц)
- Поставок получено
@ -1761,11 +2194,13 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
- Удовлетворенность клиентов (средний рейтинг)
**2. ОТГРУЗКА НА ПЛОЩАДКИ** (`marketplaces: true`):
- Отправлено на Wildberries
- Отправлено на Ozon
- Отправлено на другие площадки
**3. АНАЛИТИКА ПРОИЗВОДИТЕЛЬНОСТИ** (`performance: false`):
- Среднее время обработки (на единицу товара)
- Уровень брака (от общего объема)
- Уровень возвратов (возвраты с площадок)
@ -1773,29 +2208,159 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
### 11.7 Услуги фулфилмента (`/services`)
#### 11.7.1 Структура: 3 обязательные вкладки
#### 11.7.1 Архитектура интеграции с системой
**СВЯЗЬ С РЕЦЕПТУРАМИ СЕЛЛЕРОВ:**
```
СЕЛЛЕР (создание поставки)
└── Рецептура
├── Товар (от поставщика)
├── Услуги фулфилмента ← CRUD в разделе Услуги
├── Расходники селлера
└── Расходники фулфилмента ← ТОЛЬКО с установленной ценой
ФУЛФИЛМЕНТ (обработка)
├── Входящие поставки → Поставки расходников (создание)
└── Услуги → Расходники (установка цены за единицу)
```
#### 11.7.2 Структура: 3 обязательные вкладки
**A) 🛠️ УСЛУГИ** (`defaultValue="services"`):
- **CRUD операции**: создание, редактирование, удаление услуг
- **Управление ценами** и описаниями
- **Загрузка изображений** услуг (`imageUrl`)
- **Полный CRUD**: создание, редактирование, удаление услуг
- **Поля**: `name`, `description`, `price`, `imageUrl`
- **Glass Upload Zone**: элегантная загрузка изображений
- **Назначение**: каталог услуг для рецептур селлеров
- **GraphQL**: `GET_MY_SERVICES`, `CREATE_SERVICE`, `UPDATE_SERVICE`, `DELETE_SERVICE`
**B) 🚚 ЛОГИСТИКА**:
- **Создание маршрутов доставки** (откуда куда)
- **Тарификация**: цена до ³ и свыше ³
- **Полный CRUD**: маршруты доставки
- **Поля**: откуда → куда, тарификация до/свыше 1м³
- **Группированные локации**:
- Мой фулфилмент (название организации)
- Рынки (предустановленные)
- Склады Wildberries
- Склады Ozon
- **Glass Upload Zone**: для изображений маршрутов
- **GraphQL**: `GET_MY_LOGISTICS`, `CREATE_LOGISTICS`, `UPDATE_LOGISTICS`, `DELETE_LOGISTICS`
**C) 📦 РАСХОДНИКИ**:
- **Управление расходниками фулфилмента**
- **Интеграция с модулем "Услуги"** - селлеры могут использовать в услугах
- **Списание со складских остатков** при использовании
- **Стоимость включается** в стоимость услуги
**C) 📦 РАСХОДНИКИ** (**❌ БЕЗ СОЗДАНИЯ**):
- **ТОЛЬКО ПРОСМОТР** расходников с фулфилмент-склада
- **ЕДИНСТВЕННОЕ РЕДАКТИРУЕМОЕ ПОЛЕ**: `pricePerUnit` - цена за единицу для рецептур
- **UI подсветка**: "Цена за 1 {unit}" (например, "Цена за 1 шт")
- **Автоматическая синхронизация** при приеме поставки расходников
- **Glass Upload Zone**: для обновления изображений расходников
- **GraphQL**: `GET_MY_SUPPLIES` (read-only), `UPDATE_SUPPLY_PRICE`
#### 11.7.3 Workflow расходников
**ШАГ 1 - СОЗДАНИЕ**: Только через "Входящие поставки → Поставки расходников фулфилмента"
**ШАГ 2 - СИНХРОНИЗАЦИЯ**: При приеме на склад → автоматически в Услуги/Расходники
**ШАГ 3 - ЦЕНООБРАЗОВАНИЕ**: Установка цены за единицу в разделе Услуги
**ШАГ 4 - ИСПОЛЬЗОВАНИЕ**: Доступны в рецептурах селлеров
#### 11.7.4 Правила видимости в рецептурах
**В РЕЦЕПТУРАХ СЕЛЛЕРОВ ПОКАЗЫВАЮТСЯ ТОЛЬКО:**
- `isAvailable = true` (есть на skladе)
- `pricePerUnit != null` (цена установлена)
**НЕ ПОКАЗЫВАЮТСЯ:**
- Расходники без цены (`pricePerUnit = null`)
- Удаленные со склада (`isAvailable = false`)
**В РАЗДЕЛЕ УСЛУГИ/РАСХОДНИКИ ВИДНЫ ВСЕ:**
- С визуальной индикацией состояния (активные/неактивные/без цены)
#### 11.7.5 Разделение цен закупки и продажи
**КРИТИЧЕСКОЕ ПРАВИЛО**: Расходники фулфилмента имеют **ДВЕ РАЗНЫЕ ЦЕНЫ** для разных бизнес-процессов:
1. **ЦЕНА ЗАКУПКИ** (`Supply.price`) - цена, по которой фулфилмент купил расходник у поставщика
2. **ЦЕНА ПРОДАЖИ** (`Supply.pricePerUnit`) - цена, по которой фулфилмент продает расходник селлерам
**ПОЛЯ В БАЗЕ ДАННЫХ**:
```prisma
model Supply {
price Decimal @db.Decimal(10, 2) // Цена закупки у поставщика (НЕИЗМЕННАЯ)
pricePerUnit Decimal? @db.Decimal(10, 2) // Цена продажи селлерам (устанавливается фулфилментом)
}
```
**ПРАВИЛА ОТОБРАЖЕНИЯ ПО РАЗДЕЛАМ**:
**РАЗДЕЛ "СКЛАД → РАСХОДНИКИ ФУЛФИЛМЕНТА"**:
- Показывает `Supply.price` (цена закупки)
- Цена ТОЛЬКО ДЛЯ ЧТЕНИЯ, нельзя изменять
- Отражает историческую стоимость приобретения
**РАЗДЕЛ "УСЛУГИ → РАСХОДНИКИ"**:
- Показывает и редактирует `Supply.pricePerUnit` (цена продажи)
- Единственное место где можно изменить цену для селлеров
- Влияет на рецептуры и расчеты для селлеров
**БИЗНЕС-ЛОГИКА СОЗДАНИЯ**:
ПРИ ПОСТУПЛЕНИИ ОТ ПОСТАВЩИКА:
```typescript
const supply = await prisma.supply.create({
data: {
price: item.price, // Цена поставщика → ЗАФИКСИРОВАНА
pricePerUnit: null, // Цена продажи → ПУСТАЯ
},
})
```
УСТАНОВКА ЦЕНЫ ПРОДАЖИ (в разделе "Услуги"):
```typescript
const updated = await prisma.supply.update({
data: {
pricePerUnit: newPrice, // ТОЛЬКО цена продажи
// price НЕ ТРОГАЕМ - остается цена закупки
},
})
```
#### 11.7.6 Технические требования
**GraphQL типы:**
```graphql
# Для рецептур - только доступные с ценой
getAvailableSuppliesForRecipe: [SupplyForRecipe!]!
# В разделе Услуги - все расходники
getMySupplies: [Supply!]!
type Supply {
pricePerUnit: Float # Может быть null
unit: String! # "шт", "кг", "м"
isAvailable: Boolean! # Статус на складе
warehouseConsumableId: ID! # Связь со складом
}
```
**Экономический учет:**
- Создание: через поставки расходников
- Ценообразование: в разделе Услуги
- Списание: со склада при использовании
- Стоимость = количество × цена за единицу
### 11.8 Сотрудники фулфилмента (`/employees`)
@ -1804,17 +2369,20 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
**A) 👥 СОТРУДНИКИ** (`defaultValue="combined"`):
**Управление персоналом**:
- **CRUD операции**: создание, редактирование, удаление сотрудников
- **Статусы сотрудников**: `ACTIVE`, `VACATION`, `SICK`, `FIRED`
- **Формы добавления**: Компактная (`showCompactForm`) / Полная форма
- **Поиск и фильтрация** по имени, должности, статусу
**Табель рабочего времени**:
- **Навигация по месяцам**: текущий год/месяц с кнопками ←/→
- **Отметки по дням**: статус дня и количество отработанных часов
- **GraphQL**: `GET_EMPLOYEE_SCHEDULE`, `UPDATE_EMPLOYEE_SCHEDULE`
**B) 📋 ОТЧЕТЫ** (`value="reports"`):
- **Сводные отчеты** по сотрудникам за период
- **Экспорт данных** табеля
- **Аналитика рабочего времени**
@ -1927,11 +2495,13 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
### 13.1 Основы системы партнерства
**ПРИНЦИП РАБОТЫ**:
- Все типы кабинетов могут создавать партнерские отношения
- Партнерство реализовано через таблицы `Counterparty` и `CounterpartyRequest`
- Двустороннее партнерство: каждая организация видит другую в разделе "Партнеры"
**ТИПЫ ОРГАНИЗАЦИЙ-ПАРТНЕРОВ**:
- `WHOLESALE` - Поставщики товаров и расходников
- `FULFILLMENT` - Фулфилмент-центры
- `LOGIST` - Логистические компании
@ -1942,6 +2512,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### **СПОСОБ 1: Через заказ в маркете (автоматическое партнерство)**
**WORKFLOW**:
1. Поставщик создает товар → товар попадает в глобальный маркет
2. Селлер/Фулфилмент находит товар в маркете
3. Создает заказ (`SupplyOrder`) → статус `PENDING`
@ -1955,6 +2526,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### **СПОСОБ 2: Через раздел "Партнеры" (заявочная система)**
**WORKFLOW**:
1. Любая организация идет в раздел "Партнеры"
2. Использует поиск для нахождения нужной организации
3. Отправляет заявку на партнерство → создается `CounterpartyRequest`:
@ -1967,6 +2539,7 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
6. **Автоматически создается двустороннее партнерство** (аналогично способу 1)
**СТАТУСЫ ЗАЯВОК**:
- `PENDING` - Ожидает рассмотрения
- `ACCEPTED` - Принята (партнерство создано)
- `REJECTED` - Отклонена
@ -1977,11 +2550,13 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### **В форме создания поставки товаров через поставщиков**
**ПРАВИЛО ОТОБРАЖЕНИЯ ПОСТАВЩИКОВ**:
- Показываются только партнеры с типом `WHOLESALE`
- Источник: таблица `Counterparty` where `counterparty.type === "WHOLESALE"`
- Фильтрация по `organizationId` текущего пользователя
**ЛОГИКА РАБОТЫ**:
1. Пользователь выбирает поставщика из dropdown партнеров-поставщиков
2. Загружается каталог товаров поставщика из `Product` таблицы
3. Товары фильтруются по `organizationId = поставщик.id`
@ -1990,20 +2565,24 @@ height: calc(100vh - headerHeight - tabsHeight - statsHeight - margins)
#### **В других разделах системы**
**ВЫБОР ФУЛФИЛМЕНТ-ЦЕНТРА**:
- Партнеры с типом `FULFILLMENT`
- Используется при создании поставок расходников
**ВЫБОР ЛОГИСТИКИ**:
- Партнеры с типом `LOGIST`
- Используется при планировании доставок
**МЕССЕНДЖЕР**:
- Общение доступно только между партнерами
- Список чатов формируется из таблицы `Counterparty`
### 13.4 Технические правила
**СОЗДАНИЕ ЗАПИСЕЙ В COUNTERPARTY**:
```sql
-- При создании партнерства создаются ДВЕ записи
INSERT INTO counterparties (organizationId, counterpartyId) VALUES (org1_id, org2_id);
@ -2011,28 +2590,30 @@ INSERT INTO counterparties (organizationId, counterpartyId) VALUES (org2_id, org
```
**ПРОВЕРКА ПАРТНЕРСТВА**:
```typescript
const isPartner = await prisma.counterparty.findFirst({
where: {
organizationId: currentOrgId,
counterpartyId: targetOrgId
}
});
counterpartyId: targetOrgId,
},
})
```
**ПОЛУЧЕНИЕ ПАРТНЕРОВ ПО ТИПУ**:
```typescript
const wholesalePartners = await prisma.counterparty.findMany({
where: {
organizationId: currentOrgId,
counterparty: {
type: "WHOLESALE"
}
type: 'WHOLESALE',
},
},
include: {
counterparty: true
}
});
counterparty: true,
},
})
```
### 13.5 Решение распространенных проблем
@ -2040,6 +2621,7 @@ const wholesalePartners = await prisma.counterparty.findMany({
#### **ПРОБЛЕМА: GraphQL запрос не возвращает данные партнеров**
**Симптомы**:
- В консоли браузера: `All counterparties: 0`, `All counterparties data: []`
- GraphQL запрос отправляется успешно, но возвращает пустой массив
- В базе данных партнеры существуют
@ -2047,13 +2629,15 @@ const wholesalePartners = await prisma.counterparty.findMany({
**Возможные причины и решения**:
1. **НЕПРАВИЛЬНОЕ ИМЯ ПОЛЯ В КОДЕ** (наиболее частая ошибка):
```typescript
// ❌ НЕПРАВИЛЬНО
const allCounterparties = counterpartiesData?.getMyCounterparties || [];
const allCounterparties = counterpartiesData?.getMyCounterparties || []
// ✅ ПРАВИЛЬНО
const allCounterparties = counterpartiesData?.myCounterparties || [];
const allCounterparties = counterpartiesData?.myCounterparties || []
```
**Объяснение**: В GraphQL схеме поле называется `myCounterparties`, а не `getMyCounterparties`
2. **НЕСООТВЕТСТВИЕ ID ПОЛЬЗОВАТЕЛЯ**:
@ -2061,11 +2645,12 @@ const wholesalePartners = await prisma.counterparty.findMany({
- Убедиться что `context.user.id` соответствует ожидаемому пользователю
3. **ПРОБЛЕМЫ С КЕШИРОВАНИЕМ APOLLO CLIENT**:
```typescript
const { data, loading, error } = useQuery(GET_MY_COUNTERPARTIES, {
fetchPolicy: 'network-only', // Обходим кеш
errorPolicy: 'all'
});
errorPolicy: 'all',
})
```
4. **ОТСУТСТВИЕ ЛОГИРОВАНИЯ В РЕЗОЛВЕРЕ**:
@ -2073,6 +2658,7 @@ const wholesalePartners = await prisma.counterparty.findMany({
- Проверить что резолвер вызывается
**Чек-лист для диагностики**:
- [ ] Проверить правильность имени поля в коде (`myCounterparties`)
- [ ] Убедиться что пользователь авторизован
- [ ] Проверить логи сервера на вызов резолвера
@ -2206,6 +2792,14 @@ const wholesalePartners = await prisma.counterparty.findMany({
19. ❌ **Показывать расходники в формах создания поставок товаров** (строгая типизация `PRODUCT`/`CONSUMABLE`)
20. ❌ **Фильтровать предметы по типу на фронтенде** (фильтрация должна быть в GraphQL резолвере)
21. ❌ **ИСПОЛЬЗОВАТЬ МОКОВЫЕ ДАННЫЕ БЕЗ РАЗРЕШЕНИЯ** - все компоненты ОБЯЗАТЕЛЬНО должны использовать реальные GraphQL запросы. Моковые данные можно добавлять ТОЛЬКО с явного разрешения пользователя
22. ❌ **ДОБАВЛЯТЬ ПОЛЕ РЫНКА К ТОВАРАМ** - рынок принадлежит организации поставщика (`Organization.market`), товары наследуют рынок через связь с организацией
23. ❌ **ПУТАТЬ РЫНОК И МАРКЕТ** - РЫНОК = физическое место (Садовод, ТЯК), МАРКЕТ = раздел системы (/market)
24. ❌ **ИСПОЛЬЗОВАТЬ НАЗВАНИЯ ОРГАНИЗАЦИЙ В ЛОГИКЕ БЕЗОПАСНОСТИ** - проверки доступа только по `organization.type` и системным ID
25. ❌ **СОЗДАВАТЬ УСЛОВИЯ НА ОСНОВЕ ПОЛЬЗОВАТЕЛЬСКИХ СТРОК** - никаких `if (name.includes())` для определения функционала
26. ❌ **ПУТАТЬ ДАННЫЕ И ФУНКЦИОНАЛ** - "ОПТ Маркет" (название рынка) ≠ "Маркет" (раздел системы)
27. ❌ **ПРЕДСТАВЛЯТЬ ИНТЕРПРЕТАЦИИ КАК ФАКТЫ** - всегда четко разделять прямые цитаты из правил и логические выводы
28. ❌ **ОТВЕЧАТЬ БЕЗ ССЫЛОК НА ИСТОЧНИКИ** - при ссылке на правила всегда указывать номер строки или раздел
29. ❌ **ИСПОЛЬЗОВАТЬ КАТЕГОРИЧНЫЕ УТВЕРЖДЕНИЯ БЕЗ ДОКАЗАТЕЛЬСТВ** - избегать "ТОЧНО!", "ИМЕННО ТАК!" без прямых цитат
### 17.2 ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА:
@ -2219,6 +2813,224 @@ const wholesalePartners = await prisma.counterparty.findMany({
8. ✅ Проверка доступности товаров перед заказом
9. ✅ Соблюдение жизненного цикла статусов поставок
10. ✅ Фиксация план/факт в процессе создания продукта
11. ✅ **УКАЗЫВАТЬ ИСТОЧНИКИ ИНФОРМАЦИИ** - при ссылке на правила обязательно указывать строку/раздел
12. ✅ **РАЗДЕЛЯТЬ ФАКТЫ И ИНТЕРПРЕТАЦИИ** - четко маркировать что взято из правил, а что является выводом
13. ✅ **ИСПОЛЬЗОВАТЬ ОСТОРОЖНЫЕ ФОРМУЛИРОВКИ** - "согласно правилам", "возможно", "требует уточнения"
### 17.3 📝 ОБЯЗАТЕЛЬНЫЙ ФОРМАТ ОТВЕТОВ С ФАКТАМИ
**При ссылке на правила ОБЯЗАТЕЛЬНО использовать формат:**
✅ **ПРАВИЛЬНО:**
```
📖 ФАКТ из rules-complete.md (строка 2225): "установка цены за единицу"
🧠 МОЯ ИНТЕРПРЕТАЦИЯ: возможно, это происходит в разделе X
❓ ПРЕДПОЛОЖЕНИЕ: требует уточнения у пользователя
⚠️ НЕ НАЙДЕНО в правилах: информация о точном местоположении
```
❌ **НЕПРАВИЛЬНО:**
```
"Да! Точно понимаю! Фулфилмент устанавливает цены в разделе X!"
```
**ОБЯЗАТЕЛЬНАЯ МАРКИРОВКА:**
- 📖 **ФАКТ** - прямая цитата из правил с номером строки
- 🧠 **ИНТЕРПРЕТАЦИЯ** - мой логический вывод (четко обозначен)
- ❓ **ПРЕДПОЛОЖЕНИЕ** - гипотеза, требующая подтверждения
- ⚠️ **НЕ НАЙДЕНО** - информация отсутствует в правилах
**СТОП-СЛОВА (избегать без доказательств):**
❌ "ТОЧНО!", "ИМЕННО ТАК!", "ДА! ПОНИМАЮ!", "АБСОЛЮТНО ВЕРНО!"
✅ "Согласно правилам...", "Не указано, но возможно...", "Требует уточнения"
### 17.4 🔒 ПРАВИЛА БЕЗОПАСНОСТИ: Разделение данных и функционала
#### КРИТИЧЕСКОЕ ПРАВИЛО БЕЗОПАСНОСТИ
**ПРИНЦИП**: Названия организаций, рынков и любые пользовательские данные НИКОГДА не должны влиять на функционал и безопасность системы.
**ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА:**
✅ **ПРАВИЛЬНЫЕ ПРОВЕРКИ:**
- Проверки доступа ТОЛЬКО по типу организации: `organization.type === 'WHOLESALE'`
- Роутинг ТОЛЬКО по предопределенным путям: `/market`, `/supplies` и т.д.
- Валидация ТОЛЬКО по ID и системным полям
- Фильтрация ТОЛЬКО по enum значениям из схемы
❌ **ЗАПРЕЩЕННЫЕ ПРОВЕРКИ (УЯЗВИМОСТИ):**
- Использование `organization.name` в условиях доступа
- Проверки по `organization.market` для определения функционала
- Любые проверки содержимого строк: `includes()`, `startsWith()`, `match()`
- Динамическое создание путей на основе пользовательских данных
**ПРИМЕРЫ:**
```typescript
// ❌ УЯЗВИМОСТЬ - название может быть любым
if (organization.name.includes('Маркет')) {
// предоставить специальный доступ
}
// ❌ УЯЗВИМОСТЬ - пользователь может подделать название
if (organization.market === 'special-market') {
// изменить цены
}
// ✅ БЕЗОПАСНО - проверка по системному типу
if (organization.type === 'WHOLESALE') {
// логика для поставщиков
}
// ✅ БЕЗОПАСНО - проверка по ID из whitelist
if (ALLOWED_FULFILLMENT_IDS.includes(organization.id)) {
// логика для проверенных фулфилментов
}
```
**РАЗДЕЛЕНИЕ КОНТЕКСТОВ:**
1. **ДАННЫЕ (могут быть любыми):**
- Названия организаций: "ОПТ Маркет", "Супер Склад", и т.д.
- Названия рынков: "Садовод", "ТЯК Москва", любые другие
- Любые пользовательские строки
2. **ФУНКЦИОНАЛ (строго определен):**
- Системные разделы: `/market`, `/supplies`, `/partners`
- Типы организаций: `WHOLESALE`, `SELLER`, `FULFILLMENT`, `LOGIST`
- Статусы и enum из Prisma схемы
**ПРАВИЛО**: Физический рынок "ОПТ Маркет" - это просто строка данных. Раздел "Маркет" (/market) - это системный функционал. Они никак не связаны и не должны влиять друг на друга.
### 17.5 📦 УПРАВЛЕНИЕ СВЯЗЯМИ ТОВАР-КАРТОЧКА В РЕЦЕПТУРЕ
#### 17.5.1 Общие принципы
**НАЗНАЧЕНИЕ**: Связь товара с карточкой маркетплейса - это метаданные для учета, НЕ влияющие на физический состав продукта.
**ФОРМУЛА ПРОДУКТА НЕИЗМЕННА**:
```
ПРОДУКТ = Товар + Услуга(и) + Расходники селлера + Расходники ФФ
```
**СВЯЗЬ С МП** = отдельные метаданные для логистики и учета
#### 17.5.2 UI компонент связи с карточками
**РАСПОЛОЖЕНИЕ**: В форме создания поставки, в секции каждого товара
**ТИП КОМПОНЕНТА**: Dropdown с поиском и фильтрацией
**ИСТОЧНИК ДАННЫХ**: База данных карточек маркетплейсов селлера (GraphQL запрос)
#### 17.5.3 Логика состояний карточек
**✅ СВЯЗАНО** - карточка уже привязана к этому товару:
- Показывать зеленую галочку
- Текст: "Название карточки - Связано"
- Можно отвязать (сброс в "Без привязки")
**⚠️ ДОСТУПНО** - карточка свободна для привязки:
- Показывать желтый значок предупреждения
- Текст: "Название карточки - Доступно"
- Можно привязать к текущему товару
**❌ ЗАНЯТО** - карточка привязана к другому товару:
- Показывать красный крестик
- Текст: "Название карточки - Занято (товар: 'Название')"
- Пункт заблокирован (disabled)
- Показывать для информации, но нельзя выбрать
**🔍 БЕЗ ПРИВЯЗКИ** - товар не связан с карточкой:
- Пункт по умолчанию
- Показывать серый значок
- Текст: "Без привязки к карточке"
#### 17.5.4 Техническая реализация
**GraphQL запрос**:
```graphql
query GetSellerCards {
myMarketplaceCards {
id
title
marketplace
article
linkedProductId # null если свободна
linkedProduct {
# для отображения занятости
id
name
}
}
}
```
**Логика фильтрации**:
- Все карточки селлера показываются в dropdown
- Статус определяется по полю `linkedProductId`
- Автосвязка: карточки с похожим названием показываются первыми
**Сохранение**:
- При создании поставки связь сохраняется в поле `marketplaceCardId` рецептуры
- При изменении связи обновляется поле `linkedProductId` в карточке
#### 17.5.5 UX поведение
**ПОИСК В DROPDOWN**:
- Фильтрация по названию карточки
- Фильтрация по артикулу маркетплейса
- Автофокус при открытии
**ГРУППИРОВКА**:
```
[Dropdown: Выберите карточку Wildberries ▼]
├─ 🔍 БЕЗ ПРИВЯЗКИ
├─ ────── ДОСТУПНЫЕ ──────
├─ ⚠️ "Кроссовки Nike Air" - Доступно
├─ ⚠️ "Футболка Adidas" - Доступно
├─ ────── СВЯЗАННЫЕ ──────
├─ ✅ "Джинсы Levi's" - Связано
├─ ────── ЗАНЯТЫЕ ──────
└─ ❌ "Куртка Puma" - Занято (товар "Верхняя одежда") [disabled]
```
**ВАЛИДАЦИЯ**:
- Связь опциональна - можно создать поставку без привязки
- При выборе занятой карточки показывать предупреждение
- При отвязке подтверждать действие
#### 17.5.6 Интеграция с существующими правилами
**СОВМЕСТИМОСТЬ**:
- Не нарушает существующую логику создания поставок
- Дополняет рецептуру метаданными
- Совместима с типами поставок (карточки/поставщики)
**ОБЯЗАТЕЛЬНОСТЬ**:
- Связь с карточкой - ОПЦИОНАЛЬНА
- Товар может существовать без привязки к МП
- Карточка может существовать без привязки к товару
**ПРИОРИТЕТ РАЗРАБОТКИ**: Средний (не блокирует основную функциональность)
---
@ -2238,15 +3050,15 @@ export const GET_MY_COUNTERPARTIES = gql`
type
}
}
`;
`
// Использование в компоненте
const allCounterparties = counterpartiesData?.myCounterparties || [];
const allCounterparties = counterpartiesData?.myCounterparties || []
```
```typescript
// ❌ НЕПРАВИЛЬНО - не соответствует схеме
const allCounterparties = counterpartiesData?.getMyCounterparties || []; // Ошибка!
const allCounterparties = counterpartiesData?.getMyCounterparties || [] // Ошибка!
```
### 18.2 Правила отладки GraphQL
@ -2264,13 +3076,13 @@ const allCounterparties = counterpartiesData?.getMyCounterparties || []; // Ош
```typescript
const { data, loading, error } = useQuery(QUERY_NAME, {
fetchPolicy: 'network-only', // Обходим кеш при отладке
errorPolicy: 'all' // Показываем все ошибки
});
errorPolicy: 'all', // Показываем все ошибки
})
// Логирование для отладки
console.log("Data:", data);
console.log("Loading:", loading);
console.log("Error:", error);
console.log('Data:', data)
console.log('Loading:', loading)
console.log('Error:', error)
```
### 18.4 TypeScript Rules
@ -2282,22 +3094,22 @@ console.log("Error:", error);
```typescript
// ✅ ПРАВИЛЬНО - соответствует schema.prisma
interface GoodsProduct {
id: string;
name: string;
article: string; // <- соответствует полю в schema
quantity?: number; // <- соответствует полю в schema
id: string
name: string
article: string // <- соответствует полю в schema
quantity?: number // <- соответствует полю в schema
organization: {
id: string;
name: string;
};
id: string
name: string
}
}
```
```typescript
// ❌ НЕПРАВИЛЬНО - не соответствует schema
interface GoodsProduct {
sku: string; // <- в schema поле называется 'article'
stock?: number; // <- в schema поле называется 'quantity'
sku: string // <- в schema поле называется 'article'
stock?: number // <- в schema поле называется 'quantity'
}
```
@ -2317,10 +3129,122 @@ interface GoodsProduct {
- **Использовать параметризованные запросы** (`organizationId`, `type`, `search`) вместо фильтрации на фронтенде
- **Добавлять подробное логирование** в резолверы для отладки (входные параметры, результаты фильтрации)
- **Типы запросов должны отражать бизнес-логику**: `organizationProducts` для товаров конкретной организации
### 18.7 Правила РЫНКОВ и МАРКЕТА
**🔍 КРИТИЧЕСКОЕ РАЗДЕЛЕНИЕ:**
- **РЫНОК** 🏪 = физическое место (Садовод, ТЯК)
- **МАРКЕТ** 🛒 = раздел системы `/market`
**ПОЛЕ РЫНКА В SCHEMA:**
- **Organization.market** ✅ - поставщик принадлежит физическому рынку
- **Product.market** ❌ - ЗАПРЕЩЕНО, товары наследуют рынок от организации
- **Отображение рынка товаров**: через `product.organization.market`
- **Фильтрация по рынкам**: через `organization.market`, НЕ через `product.market`
**ЗАПРОСЫ С РЫНКАМИ:**
```graphql
# ✅ ПРАВИЛЬНО - рынок от организации поставщика
query GetProductsWithMarket {
myProducts {
id
name
organization {
market # Физический рынок поставщика
}
}
}
# ✅ ПРАВИЛЬНО - товары в маркете с информацией о рынке
query GetMarketProducts {
marketProducts {
id
name
organization {
market # Рынок поставщика
name # Название поставщика
}
}
}
```
**МАРКЕТ (/market) ПРАВИЛА:**
- **Назначение**: Глобальный каталог всех товаров
- **Фильтрация**: По рынкам поставщиков, типам товаров, категориям
- **Отображение**: Показать рынок поставщика в карточках товаров
- **НЕ путать**: МАРКЕТ ≠ конкретный физический рынок
- **Значения по умолчанию в резолверах** для критических параметров (`type: args.type || "PRODUCT"`)
- **Валидация обязательных параметров** на уровне схемы (`organizationId: ID!`)
- **Кеширование обходить при проблемах** через `fetchPolicy: 'network-only'`
### 18.8 GraphQL правила для поля organization в мутациях
#### 18.8.1 Обязательность поля organization
**ПРАВИЛО**: Все мутации, возвращающие объекты с типом, включающим `organization: Organization!`, ДОЛЖНЫ запрашивать это поле.
**ПРОБЛЕМА**: Apollo Client кэш ожидает поле `organization` в ответе, если оно определено в GraphQL типе как обязательное.
#### 18.8.2 Правильное написание мутаций
**❌ НЕПРАВИЛЬНО** (вызывает ошибку Apollo Client):
```graphql
mutation UpdateLogistics($id: ID!, $input: LogisticsInput!) {
updateLogistics(id: $id, input: $input) {
success
logistics {
id
fromLocation
# НЕТ поля organization - ОШИБКА кэша!
}
}
}
```
**✅ ПРАВИЛЬНО** (работает корректно):
```graphql
mutation UpdateLogistics($id: ID!, $input: LogisticsInput!) {
updateLogistics(id: $id, input: $input) {
success
logistics {
id
fromLocation
organization {
# ОБЯЗАТЕЛЬНО включить!
id
name
fullName
}
}
}
}
```
#### 18.8.3 Чек-лист для мутаций
**ОБЯЗАТЕЛЬНАЯ ПРОВЕРКА** перед созданием мутации:
1. ✅ Проверить GraphQL тип возвращаемого объекта
2. ✅ Если есть поле `organization: Organization!` - добавить в запрос
3. ✅ Включить минимальные поля: `id`, `name`, `fullName`
4. ✅ Проверить resolver включает `include: { organization: true }`
**ПРИМЕНЯЕТСЯ К**:
- `CREATE_LOGISTICS` ✅ Исправлено
- `UPDATE_LOGISTICS` ✅ Исправлено
- `CREATE_SERVICE` - проверить при разработке
- `UPDATE_SERVICE` - проверить при разработке
- Все другие мутации с организационными объектами
**ОШИБКА БЕЗ ПОЛЯ**: `Error converting field "organization" of expected non-nullable type`
---
## 19. 🔧 АРХИТЕКТУРНЫЕ ПРИНЦИПЫ
@ -2444,6 +3368,7 @@ interface GoodsProduct {
### 22.2 12 специализированных категорий расходников
#### 🎁 **1. УПАКОВКА И ЗАЩИТА**
- Коробки (различных размеров)
- Пакеты (полиэтиленовые, бумажные, фирменные)
- Пузырчатая пленка, воздушные подушки
@ -2451,6 +3376,7 @@ interface GoodsProduct {
- Паллетная пленка, защитные уголки
#### 🏷️ **2. МАРКИРОВКА И ИДЕНТИФИКАЦИЯ**
- Этикетки (адресные, штрих-код, QR-код)
- Бирки (ценники, размерники)
- Стикеры и наклейки
@ -2458,6 +3384,7 @@ interface GoodsProduct {
- Штампы и печати, термоэтикетки
#### 🔧 **3. КРЕПЕЖ И СОЕДИНЕНИЕ**
- Скотч (прозрачный, цветной, армированный)
- Клей и клеевые составы
- Стяжки пластиковые
@ -2465,6 +3392,7 @@ interface GoodsProduct {
- Веревки и шнуры, стрейч-лента
#### 📄 **4. ДОКУМЕНТООБОРОТ И ВКЛАДЫШИ**
- Накладные и сопроводительные документы
- Инструкции по эксплуатации
- Гарантийные талоны
@ -2472,6 +3400,7 @@ interface GoodsProduct {
- Благодарственные письма, купоны и промокоды
#### 🧼 **5. ГИГИЕНА И БЕЗОПАСНОСТЬ**
- Перчатки (латексные, нитриловые)
- Маски и респираторы
- Антисептики и дезинфекторы
@ -2479,6 +3408,7 @@ interface GoodsProduct {
- Фартуки и халаты, бахилы
#### 🛠️ **6. ИНСТРУМЕНТЫ И ПРИСПОСОБЛЕНИЯ**
- Ножи и резаки, ножницы
- Линейки и рулетки
- Упаковочные машины (ленточные)
@ -2487,6 +3417,7 @@ interface GoodsProduct {
- Весы и мерная тара
#### 🎨 **7. БРЕНДИНГ И ДИЗАЙН**
- Фирменные пакеты с логотипом
- Брендированные коробки
- Цветная упаковочная бумага
@ -2495,6 +3426,7 @@ interface GoodsProduct {
- Подарочная упаковка
#### ⚡ **8. СПЕЦИАЛИЗИРОВАННЫЕ МАТЕРИАЛЫ**
- Антистатические пакеты
- Влагопоглотители
- Температурные индикаторы
@ -2503,6 +3435,7 @@ interface GoodsProduct {
- Защита от краж (магнитные датчики)
#### 🏪 **9. ТОРГОВОЕ ОБОРУДОВАНИЕ**
- Манекены и вешалки
- Ценникодержатели
- Подставки и стойки
@ -2511,6 +3444,7 @@ interface GoodsProduct {
- Освещение витрин
#### 🚚 **10. ЛОГИСТИКА И СКЛАДИРОВАНИЕ**
- Паллеты и поддоны
- Контейнеры и ящики
- Стеллажные системы
@ -2519,6 +3453,7 @@ interface GoodsProduct {
- Адресные ярлыки для груза
#### 💻 **11. ТЕХНИЧЕСКИЕ РАСХОДНИКИ**
- Картриджи для принтеров
- Термоголовки, красящие ленты
- Батарейки для сканеров
@ -2526,6 +3461,7 @@ interface GoodsProduct {
- Запчасти для упаковочного оборудования
#### 🎪 **12. СЕЗОННЫЕ И ПРАЗДНИЧНЫЕ**
- Новогодняя упаковка
- Подарочные мешки
- Праздничные ленты
@ -2571,21 +3507,25 @@ interface GoodsProduct {
### 🔴 Отмена заказов на разных этапах workflow
**PENDING → Отмена разрешена**
- Действие: Удаление заказа без последствий
- Уведомления: Поставщику о отмене
- Влияние на статистику: Нет
**SUPPLIER_APPROVED → Отмена с согласия поставщика**
- Действие: Требуется подтверждение поставщика
- Штрафы: Возможны согласно договору
- Восстановление: Товары возвращаются в доступные остатки
**CONFIRMED/LOGISTICS_CONFIRMED → Отмена критическая**
- Действие: Требуется согласие всех участников
- Штрафы: Логистические расходы
- Альтернатива: Изменение адреса доставки
**SHIPPED/IN_TRANSIT → Отмена невозможна**
- Действие: Только возврат после получения
- Процедура: Через модуль "Возвраты с ПВЗ"
@ -2594,6 +3534,7 @@ interface GoodsProduct {
**Сценарий**: Поставщик доставил 80 из 100 заказанных единиц
**Алгоритм обработки**:
1. Фулфилмент фиксирует фактическое количество
2. Система создает два отдельных документа:
- DELIVERED (80 единиц) - обрабатывается обычным порядком
@ -2606,9 +3547,10 @@ interface GoodsProduct {
**Проблема**: Попытка заказать больше чем есть у поставщика
**Техническая реализация**:
```typescript
if (requestedQuantity > availableStock) {
throw new GraphQLError(`Недостаточно товара. Доступно: ${availableStock}, запрошено: ${requestedQuantity}`);
throw new GraphQLError(`Недостаточно товара. Доступно: ${availableStock}, запрошено: ${requestedQuantity}`)
}
```
@ -2619,6 +3561,7 @@ if (requestedQuantity > availableStock) {
**Сценарий**: Поставщик пытается создать товар с существующим артикулом
**Проверка на уровне БД**:
```sql
UNIQUE INDEX ON products(article, organization_id)
```
@ -2633,35 +3576,35 @@ UNIQUE INDEX ON products(article, organization_id)
```typescript
// Основные запросы
GET_MY_SERVICES; // Услуги фулфилмента
GET_MY_LOGISTICS; // Логистические маршруты
GET_MY_EMPLOYEES; // Сотрудники организации
GET_FULFILLMENT_WAREHOUSE_STATS; // Статистика склада
GET_WAREHOUSE_PRODUCTS; // Товары на складе
GET_MY_FULFILLMENT_SUPPLIES; // Расходники фулфилмента
GET_EMPLOYEE_SCHEDULE; // Табель рабочего времени
GET_MY_SERVICES // Услуги фулфилмента
GET_MY_LOGISTICS // Логистические маршруты
GET_MY_EMPLOYEES // Сотрудники организации
GET_FULFILLMENT_WAREHOUSE_STATS // Статистика склада
GET_WAREHOUSE_PRODUCTS // Товары на складе
GET_MY_FULFILLMENT_SUPPLIES // Расходники фулфилмента
GET_EMPLOYEE_SCHEDULE // Табель рабочего времени
// Мутации
CREATE_SERVICE, UPDATE_SERVICE, DELETE_SERVICE;
CREATE_LOGISTICS, UPDATE_LOGISTICS, DELETE_LOGISTICS;
CREATE_EMPLOYEE, UPDATE_EMPLOYEE, DELETE_EMPLOYEE;
UPDATE_EMPLOYEE_SCHEDULE; // Обновление табеля
;(CREATE_SERVICE, UPDATE_SERVICE, DELETE_SERVICE)
;(CREATE_LOGISTICS, UPDATE_LOGISTICS, DELETE_LOGISTICS)
;(CREATE_EMPLOYEE, UPDATE_EMPLOYEE, DELETE_EMPLOYEE)
UPDATE_EMPLOYEE_SCHEDULE // Обновление табеля
```
### Приложение B: Компоненты фулфилмента
```typescript
// Основные dashboard компоненты
FulfillmentWarehouseDashboard; // Склад фулфилмента
FulfillmentStatisticsDashboard; // Статистика
ServicesDashboard; // Услуги (3 вкладки)
EmployeesDashboard; // Сотрудники
SuppliesDashboard; // Поставки фулфилмента
FulfillmentWarehouseDashboard // Склад фулфилмента
FulfillmentStatisticsDashboard // Статистика
ServicesDashboard // Услуги (3 вкладки)
EmployeesDashboard // Сотрудники
SuppliesDashboard // Поставки фулфилмента
// Специализированные компоненты
ServicesTab, LogisticsTab, SuppliesTab; // Вкладки услуг
EmployeeInlineForm, EmployeeEditInlineForm; // Формы сотрудников
FulfillmentSuppliesTab, FulfillmentConsumablesOrdersTab; // Поставки
;(ServicesTab, LogisticsTab, SuppliesTab) // Вкладки услуг
;(EmployeeInlineForm, EmployeeEditInlineForm) // Формы сотрудников
;(FulfillmentSuppliesTab, FulfillmentConsumablesOrdersTab) // Поставки
```
### Приложение C: Специальный роутинг для типов организаций
@ -2669,31 +3612,32 @@ FulfillmentSuppliesTab, FulfillmentConsumablesOrdersTab; // Поставки
```typescript
const handleSuppliesClick = () => {
switch (user?.organization?.type) {
case "FULFILLMENT":
router.push("/fulfillment-supplies"); // Специальный роут
break;
case "SELLER":
router.push("/supplies");
break;
case "WHOLESALE":
router.push("/wholesale-supplies");
break;
case "LOGIST":
router.push("/logist-supplies");
break;
case 'FULFILLMENT':
router.push('/fulfillment-supplies') // Специальный роут
break
case 'SELLER':
router.push('/supplies')
break
case 'WHOLESALE':
router.push('/wholesale-supplies')
break
case 'LOGIST':
router.push('/logist-supplies')
break
}
};
}
```
---
_Эта база знаний создана путем объединения rules-unified.md (v3.0) и fulfillment-cabinet-rules.md (v1.0) с устранением всех несоответствий и добавлением критически важных улучшений: быстрый справочник, глоссарий терминов, детальные алгоритмы процессов, edge cases._
_Версия: 9.2_
_Версия: 10.1_
ата создания: 2025_
_Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗРАБОТКЕ_
### 🚀 УЛУЧШЕНИЯ v6.0:
- Быстрый справочник критических правил
- 🔤 Полный глоссарий терминов с определениями
- 🎯 Навигация по ролям (разработчики, аналитики, менеджеры)
@ -2703,6 +3647,7 @@ _Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗ
- 📊 Таблицы SLA и временных рамок
### 🔧 ИСПРАВЛЕНИЯ v6.1:
- Устранено противоречие в моменте создания БРАКА
- Исправлена логическая цепочка: рецептура задается селлером ДО процесса
- Реалистичные временные рамки SLA (рабочие дни вместо часов)
@ -2710,11 +3655,13 @@ _Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗ
- Согласованы все алгоритмы и процессы между разделами
### 🔧 ОБНОВЛЕНИЯ v6.3:
- **ДОБАВЛЕН КРИТИЧЕСКИЙ ЗАПРЕТ**: Использование моковых данных в продакшене
- **ОБНОВЛЕН ЧЕКЛИСТ**: Добавлена проверка на отсутствие mock данных
- **РЕАЛИЗАЦИЯ**: Полная очистка моковых данных из раздела "Мои поставки" селлера
### 🎨 ИНТЕГРАЦИЯ v6.2:
- Синхронизация с visual-design-rules.md v1.1
- Добавлены визуальные правила для 8 статусов поставок
- Создана цветовая система для 6 модулей фулфилмента
@ -2723,23 +3670,27 @@ _Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗ
- Покрытие визуальными решениями увеличено с 40% до 85%
### 🚀 КОНСОЛИДАЦИЯ v7.0:
- Интеграция development-checklist.md и CLAUDE.md
- Удаление дублирующих файлов
- Создание единого источника истины
### 🔧 ПОЛНАЯ ИНТЕГРАЦИЯ v8.0:
- Интеграция work-protocols.md (детальные протоколы по сложности)
- Интеграция violation-prevention-protocol.md (СТОП-сигналы и триггеры)
- Интеграция self-validation.md (расширенная система самопроверки)
- Удаление всех дублирующих файлов протоколов
### 🎯 ОПТИМИЗАЦИЯ UI/UX v8.1:
- Добавлен ТРИГГЕР #3 для автоматической активации visual-design-rules.md
- Интегрирован специальный UI/UX протокол в чеклист
- Создана система перекрестных ссылок с visual-design-rules.md
- visual-design-rules.md остается отдельным специализированным файлом
### 📊 ИНТЕГРАЦИЯ DESCRIPTION v9.0:
- Добавлена UI структура создания поставки расходников (3 блока)
- Интегрирована концепция многоуровневых таблиц
- Добавлен механизм учета ПЛАН/ФАКТ в процессе создания продукта
@ -2747,6 +3698,7 @@ _Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗ
- Добавлена опция места хранения готовых продуктов
### 🔧 УТОЧНЕНИЯ ЛОГИКИ v9.1:
- Уточнен статус брака: НЕ РЕАЛИЗОВАНО (еще не дошли до этого этапа)
- Добавлены четкие правила создания предметов по ролям
- Добавлен экономический учет расходников фулфилмента для селлера
@ -2754,8 +3706,25 @@ _Статус: ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ - ГОТОВ К РАЗ
- Добавлена заметка о будущей детализации статусов товаров
### 🎨 UI УЛУЧШЕНИЯ v9.2:
- Добавлены детальные правила горизонтального скролла для блока поставщиков
- Реализован горизонтальный скролл в create-suppliers-supply-page.tsx
- Добавлена адаптивность (десктоп 280px, планшет 260px, мобильный 240px)
- Интегрированы ARIA атрибуты для доступности
- Реализовано автоскрытие полосы прокрутки и навигация клавиатурой
### 🔒 БЕЗОПАСНОСТЬ И ТЕРМИНОЛОГИЯ v10.0:
- **ДОБАВЛЕН РАЗДЕЛ 17.3**: Правила безопасности - разделение данных и функционала
- **НОВЫЕ ЗАПРЕТЫ 24-26**: Запрет использования пользовательских данных в логике безопасности
- **РАСШИРЕН ГЛОССАРИЙ**: Контекстно-зависимые термины для SupplyOrder
- **УТОЧНЕНИЕ ТЕРМИНОВ**: Четкое разделение "Маркет" (раздел) vs "Маркетплейс" (внешние площадки)
- **ПРИМЕРЫ УЯЗВИМОСТЕЙ**: Конкретные примеры безопасного и небезопасного кода
### 📝 КАЧЕСТВО ОТВЕТОВ v10.1:
- **НОВЫЕ ЗАПРЕТЫ 27-29**: Запрет представления интерпретаций как фактов
- **ОБЯЗАТЕЛЬНЫЙ ФОРМАТ ОТВЕТОВ 17.3**: Четкое разделение фактов, интерпретаций и предположений
- **СИСТЕМА МАРКИРОВКИ**: 📖 ФАКТ, 🧠 ИНТЕРПРЕТАЦИЯ, ПРЕДПОЛОЖЕНИЕ, НЕ НАЙДЕНО
- **СТОП-СЛОВА**: Список категоричных утверждений для избегания без доказательств
- **ОБЯЗАТЕЛЬНЫЕ ПРАВИЛА 11-13**: Указание источников и осторожные формулировки

View File

@ -21,11 +21,7 @@ const handler = startServerAndCreateNextHandler<NextRequest, Context>(server, {
const authHeader = req.headers.get('authorization')
const token = authHeader?.replace('Bearer ', '')
console.warn('GraphQL Context - Auth header:', authHeader)
console.warn('GraphQL Context - Token:', token ? `${token.substring(0, 20)}...` : 'No token')
if (!token) {
console.warn('GraphQL Context - No token provided')
return { user: null, admin: null, prisma }
}
@ -46,10 +42,6 @@ const handler = startServerAndCreateNextHandler<NextRequest, Context>(server, {
// Проверяем тип токена
if (decoded.type === 'admin' && decoded.adminId && decoded.username) {
console.warn('GraphQL Context - Decoded admin:', {
id: decoded.adminId,
username: decoded.username,
})
return {
admin: {
id: decoded.adminId,
@ -59,10 +51,6 @@ const handler = startServerAndCreateNextHandler<NextRequest, Context>(server, {
prisma,
}
} else if (decoded.userId && decoded.phone) {
console.warn('GraphQL Context - Decoded user:', {
id: decoded.userId,
phone: decoded.phone,
})
return {
user: {
id: decoded.userId,

View File

@ -1,8 +1,6 @@
'use client'
import {
ChevronDown,
ChevronUp,
Plus,
Minus,
Star,
@ -63,7 +61,6 @@ export function InteractiveDemo() {
const [counter, setCounter] = useState(5)
const [showPassword, setShowPassword] = useState(false)
const [notifications, setNotifications] = useState(true)
const [expandedCard, setExpandedCard] = useState<number | null>(null)
const [selectedItems, setSelectedItems] = useState<number[]>([])
const [copied, setCopied] = useState(false)
@ -579,19 +576,16 @@ export function InteractiveDemo() {
{/* Расширяемые элементы */}
<Card className="glass-card border-white/10">
<CardHeader>
<CardTitle className="text-white">Расширяемые элементы</CardTitle>
<CardTitle className="text-white">Статичные элементы</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Expandable Cards */}
{/* Static Cards */}
<div>
<h4 className="text-white/90 text-sm font-medium mb-3">Расширяемые карточки</h4>
<h4 className="text-white/90 text-sm font-medium mb-3">Статичные карточки</h4>
<div className="space-y-3">
{[1, 2, 3].map((card) => (
<div key={card} className="glass-card rounded-lg border border-white/10 overflow-hidden">
<div
className="p-4 cursor-pointer flex items-center justify-between hover:bg-white/5 transition-colors"
onClick={() => setExpandedCard(expandedCard === card ? null : card)}
>
<div className="p-4 flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-blue-500/20 rounded-lg flex items-center justify-center">
<Settings className="h-5 w-5 text-blue-400" />
@ -601,37 +595,11 @@ export function InteractiveDemo() {
<div className="text-white/60 text-sm">Описание настройки {card}</div>
</div>
</div>
{expandedCard === card ? (
<ChevronUp className="h-5 w-5 text-white/60" />
) : (
<ChevronDown className="h-5 w-5 text-white/60" />
)}
</div>
{expandedCard === card && (
<div className="px-4 pb-4 border-t border-white/10">
<div className="mt-4 space-y-3">
<div className="flex items-center justify-between">
<Label className="text-white">Включить функцию</Label>
<Switch />
</div>
<div className="space-y-2">
<Label className="text-white">Уровень</Label>
<Slider defaultValue={[50]} max={100} step={1} />
</div>
<div className="flex justify-end space-x-2">
<Button variant="ghost" size="sm">
Сбросить
</Button>
<Button variant="glass" size="sm">
Применить
<Edit3 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
</div>
))}
</div>
</div>

View File

@ -19,29 +19,17 @@ export function AuthGuard({ children, fallback }: AuthGuardProps) {
useEffect(() => {
const initAuth = async () => {
if (initRef.current) {
console.warn('AuthGuard - Already initialized, skipping')
return
}
initRef.current = true
console.warn('AuthGuard - Initializing auth check')
await checkAuth()
setIsChecking(false)
console.warn('AuthGuard - Auth check completed, authenticated:', isAuthenticated, 'user:', !!user)
}
initAuth()
}, [checkAuth, isAuthenticated, user]) // Добавляем зависимости как требует линтер
// Дополнительное логирование состояний
useEffect(() => {
console.warn('AuthGuard - State update:', {
isChecking,
isLoading,
isAuthenticated,
hasUser: !!user,
})
}, [isChecking, isLoading, isAuthenticated, user])
// Показываем лоадер пока проверяем авторизацию
if (isChecking || isLoading) {
@ -57,11 +45,9 @@ export function AuthGuard({ children, fallback }: AuthGuardProps) {
// Если не авторизован, показываем форму авторизации
if (!isAuthenticated) {
console.warn('AuthGuard - User not authenticated, showing auth flow')
return fallback || <AuthFlow />
}
// Если авторизован, показываем защищенный контент
console.warn('AuthGuard - User authenticated, showing dashboard')
return <>{children}</>
}

View File

@ -83,8 +83,6 @@ export function MarketplaceApiStep({ onNext, onBack }: MarketplaceApiStepProps)
},
})
console.warn(`🎯 Client received response for ${marketplace}:`, data)
setValidationStates((prev) => ({
...prev,
[marketplace]: {
@ -113,8 +111,7 @@ export function MarketplaceApiStep({ onNext, onBack }: MarketplaceApiStepProps)
})
}
}
} catch (error) {
console.warn(`🔴 Client validation error for ${marketplace}:`, error)
} catch {
setValidationStates((prev) => ({
...prev,
[marketplace]: {

View File

@ -84,11 +84,8 @@ export function SmsStep({ phone, onNext, onBack }: SmsStepProps) {
const result = await verifySmsCode(formattedPhone, fullCode)
if (result.success) {
console.warn('SmsStep - SMS verification successful, user:', result.user)
// Проверяем есть ли у пользователя уже организация
if (result.user?.organization) {
console.warn('SmsStep - User already has organization, redirecting to dashboard')
// Если организация уже есть, перенаправляем прямо в кабинет
router.push('/dashboard')
return

View File

@ -31,6 +31,7 @@ import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { UPDATE_USER_PROFILE, UPDATE_ORGANIZATION_BY_INN } from '@/graphql/mutations'
import { GET_ME } from '@/graphql/queries'
@ -86,6 +87,9 @@ export function UserSettings() {
// API ключи маркетплейсов
wildberriesApiKey: '',
ozonApiKey: '',
// Рынок для поставщиков
market: '',
})
// Загружаем данные организации при монтировании компонента
@ -129,10 +133,13 @@ export function UserSettings() {
} = {}
try {
if (org.managementPost && typeof org.managementPost === 'string') {
// Проверяем, что строка начинается с { или [, иначе это не JSON
if (org.managementPost.trim().startsWith('{') || org.managementPost.trim().startsWith('[')) {
customContacts = JSON.parse(org.managementPost)
}
} catch (e) {
console.warn('Ошибка парсинга managementPost:', e)
}
} catch {
// Игнорируем ошибки парсинга
}
setFormData({
@ -153,6 +160,7 @@ export function UserSettings() {
corrAccount: customContacts?.bankDetails?.corrAccount || '',
wildberriesApiKey: '',
ozonApiKey: '',
market: org.market || 'none',
})
}
}, [user])
@ -289,7 +297,6 @@ export function UserSettings() {
})
// TODO: Сохранить партнерский код в базе данных
console.warn('Partner code generated:', partnerCode)
} catch (error) {
console.error('Error generating partner link:', error)
setSaveMessage({ type: 'error', text: 'Ошибка при генерации ссылки' })
@ -341,7 +348,7 @@ export function UserSettings() {
avatar: avatarUrl,
},
},
update: (cache, { data }) => {
update: (cache, { data }: { data?: any }) => {
if (data?.updateUserProfile?.success) {
// Обновляем кеш Apollo Client
try {
@ -357,8 +364,8 @@ export function UserSettings() {
},
})
}
} catch (error) {
console.warn('Cache update error:', error)
} catch {
// Игнорируем ошибки обновления кеша
}
}
},
@ -517,6 +524,7 @@ export function UserSettings() {
const handleInputChange = (field: string, value: string) => {
let processedValue = value
// Применяем маски и валидации
switch (field) {
case 'orgPhone':
@ -581,6 +589,59 @@ export function UserSettings() {
}
}
// Проверка наличия изменений в форме
const hasFormChanges = () => {
if (!user?.organization) return false
const org = user.organization
// Извлекаем текущий телефон из organization.phones
let currentOrgPhone = '+7'
if (org.phones && Array.isArray(org.phones) && org.phones.length > 0) {
currentOrgPhone = org.phones[0].value || org.phones[0] || '+7'
}
// Извлекаем текущий email из organization.emails
let currentEmail = ''
if (org.emails && Array.isArray(org.emails) && org.emails.length > 0) {
currentEmail = org.emails[0].value || org.emails[0] || ''
}
// Извлекаем дополнительные данные из managementPost
let customContacts: any = {}
try {
if (org.managementPost && typeof org.managementPost === 'string') {
// Проверяем, что строка начинается с { или [, иначе это не JSON
if (org.managementPost.trim().startsWith('{') || org.managementPost.trim().startsWith('[')) {
customContacts = JSON.parse(org.managementPost)
}
}
} catch {
// ignore parse errors
}
// Нормализуем значения для сравнения
const normalizeValue = (value: string | null | undefined) => value || ''
const normalizeMarketValue = (value: string | null | undefined) => value || 'none'
// Проверяем изменения в полях
const changes = [
normalizeValue(formData.orgPhone) !== normalizeValue(currentOrgPhone),
normalizeValue(formData.managerName) !== normalizeValue(user?.managerName),
normalizeValue(formData.telegram) !== normalizeValue(customContacts?.telegram),
normalizeValue(formData.whatsapp) !== normalizeValue(customContacts?.whatsapp),
normalizeValue(formData.email) !== normalizeValue(currentEmail),
normalizeMarketValue(formData.market) !== normalizeMarketValue(org.market),
normalizeValue(formData.bankName) !== normalizeValue(customContacts?.bankDetails?.bankName),
normalizeValue(formData.bik) !== normalizeValue(customContacts?.bankDetails?.bik),
normalizeValue(formData.accountNumber) !== normalizeValue(customContacts?.bankDetails?.accountNumber),
normalizeValue(formData.corrAccount) !== normalizeValue(customContacts?.bankDetails?.corrAccount),
]
const hasChanges = changes.some(changed => changed)
return hasChanges
}
// Проверка наличия ошибок валидации
const hasValidationErrors = () => {
const fields = [
@ -657,6 +718,7 @@ export function UserSettings() {
bik?: string
accountNumber?: string
corrAccount?: string
market?: string
} = {}
// orgName больше не редактируется - устанавливается только при регистрации
@ -669,6 +731,7 @@ export function UserSettings() {
if (formData.bik?.trim()) inputData.bik = formData.bik.trim()
if (formData.accountNumber?.trim()) inputData.accountNumber = formData.accountNumber.trim()
if (formData.corrAccount?.trim()) inputData.corrAccount = formData.corrAccount.trim()
if (formData.market) inputData.market = formData.market
const result = await updateUserProfile({
variables: {
@ -714,7 +777,6 @@ export function UserSettings() {
}
if (isNaN(date.getTime())) {
console.warn('Invalid date string:', dateString)
return 'Неверная дата'
}
@ -723,8 +785,7 @@ export function UserSettings() {
month: 'long',
day: 'numeric',
})
} catch (error) {
console.error('Error formatting date:', error, dateString)
} catch {
return 'Ошибка даты'
}
}
@ -831,9 +892,9 @@ export function UserSettings() {
<Button
size="sm"
onClick={handleSave}
disabled={hasValidationErrors() || isSaving}
disabled={hasValidationErrors() || isSaving || !hasFormChanges()}
className={`glass-button text-white cursor-pointer ${
hasValidationErrors() || isSaving ? 'opacity-50 cursor-not-allowed' : ''
hasValidationErrors() || isSaving || !hasFormChanges() ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
<Save className="h-4 w-4 mr-2" />
@ -1068,9 +1129,9 @@ export function UserSettings() {
<Button
size="sm"
onClick={handleSave}
disabled={hasValidationErrors() || isSaving}
disabled={hasValidationErrors() || isSaving || !hasFormChanges()}
className={`glass-button text-white cursor-pointer ${
hasValidationErrors() || isSaving ? 'opacity-50 cursor-not-allowed' : ''
hasValidationErrors() || isSaving || !hasFormChanges() ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
<Save className="h-4 w-4 mr-2" />
@ -1254,6 +1315,41 @@ export function UserSettings() {
</div>
</div>
)}
{/* Настройка рынка для поставщиков */}
{user?.organization?.type === 'WHOLESALE' && (
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 flex items-center gap-2">
🏪 Физический рынок
</Label>
{isEditing ? (
<Select value={formData.market || 'none'} onValueChange={(value) => handleInputChange('market', value)}>
<SelectTrigger className="glass-input text-white h-10 text-sm">
<SelectValue placeholder="Выберите рынок" />
</SelectTrigger>
<SelectContent className="glass-card">
<SelectItem value="none">Не указан</SelectItem>
<SelectItem value="sadovod" className="text-white">Садовод</SelectItem>
<SelectItem value="tyak-moscow" className="text-white">ТЯК Москва</SelectItem>
</SelectContent>
</Select>
) : (
<Input
value={formData.market && formData.market !== 'none' ?
(formData.market === 'sadovod' ? 'Садовод' :
formData.market === 'tyak-moscow' ? 'ТЯК Москва' :
formData.market) : 'Не указан'}
readOnly
className="glass-input text-white h-10 read-only:opacity-70"
/>
)}
<p className="text-white/50 text-xs mt-1">
Физический рынок, где работает поставщик. Товары наследуют рынок от организации.
</p>
</div>
</div>
)}
</div>
</Card>
</TabsContent>
@ -1295,7 +1391,7 @@ export function UserSettings() {
<Button
size="sm"
onClick={handleSave}
disabled={hasValidationErrors() || isSaving}
disabled={hasValidationErrors() || isSaving || !hasFormChanges()}
className={`glass-button text-white cursor-pointer ${
hasValidationErrors() || isSaving ? 'opacity-50 cursor-not-allowed' : ''
}`}
@ -1402,7 +1498,7 @@ export function UserSettings() {
<Button
size="sm"
onClick={handleSave}
disabled={hasValidationErrors() || isSaving}
disabled={hasValidationErrors() || isSaving || !hasFormChanges()}
className={`glass-button text-white cursor-pointer ${
hasValidationErrors() || isSaving ? 'opacity-50 cursor-not-allowed' : ''
}`}

View File

@ -12,10 +12,8 @@ import {
Phone,
Mail,
Briefcase,
DollarSign,
Calendar,
MessageCircle,
User,
} from 'lucide-react'
import { useState } from 'react'

View File

@ -21,7 +21,6 @@ import {
Truck,
Warehouse,
Eye,
EyeOff,
} from 'lucide-react'
import { useState } from 'react'

View File

@ -4,21 +4,16 @@ import { useQuery, useMutation } from '@apollo/client'
import {
ArrowLeft,
Building2,
MapPin,
Phone,
Mail,
Star,
Search,
Package,
Plus,
Minus,
ShoppingCart,
Wrench,
Box,
} from 'lucide-react'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import React, { useState, useEffect } from 'react'
import React, { useState } from 'react'
import { toast } from 'sonner'
import { OrganizationAvatar } from '@/components/market/organization-avatar'
@ -114,7 +109,7 @@ export function CreateFulfillmentConsumablesSupplyPage() {
} = useQuery(GET_ORGANIZATION_PRODUCTS, {
skip: !selectedSupplier,
variables: {
organizationId: selectedSupplier.id,
organizationId: selectedSupplier?.id,
search: productSearchQuery || null,
category: null,
type: 'CONSUMABLE', // Фильтруем только расходники согласно rules2.md
@ -122,7 +117,7 @@ export function CreateFulfillmentConsumablesSupplyPage() {
onCompleted: (data) => {
console.warn('✅ GET_ORGANIZATION_PRODUCTS COMPLETED:', {
totalProducts: data?.organizationProducts?.length || 0,
organizationId: selectedSupplier.id,
organizationId: selectedSupplier?.id,
type: 'CONSUMABLE',
products:
data?.organizationProducts?.map((p) => ({
@ -203,14 +198,6 @@ export function CreateFulfillmentConsumablesSupplyPage() {
}).format(amount)
}
const renderStars = (rating: number = 4.5) => {
return Array.from({ length: 5 }, (_, i) => (
<Star
key={i}
className={`h-3 w-3 ${i < Math.floor(rating) ? 'text-yellow-400 fill-current' : 'text-gray-400'}`}
/>
))
}
const updateConsumableQuantity = (productId: string, quantity: number) => {
const product = supplierProducts.find((p: FulfillmentConsumableProduct) => p.id === productId)

View File

@ -12,9 +12,7 @@ import { useSidebar } from '@/hooks/useSidebar'
// Импорты компонентов подразделов
import { FulfillmentConsumablesOrdersTab } from './fulfillment-supplies/fulfillment-consumables-orders-tab'
import { FulfillmentDetailedSuppliesTab } from './fulfillment-supplies/fulfillment-detailed-supplies-tab'
import { FulfillmentSuppliesTab } from './fulfillment-supplies/fulfillment-supplies-tab'
import { PvzReturnsTab } from './fulfillment-supplies/pvz-returns-tab'
import { MarketplaceSuppliesTab } from './marketplace-supplies/marketplace-supplies-tab'
// Компонент для отображения бейджа с уведомлениями
function NotificationBadge({ count }: { count: number }) {

View File

@ -5,10 +5,8 @@ import {
Calendar,
Package,
Truck,
User,
CheckCircle,
Clock,
AlertCircle,
XCircle,
MapPin,
Phone,
@ -17,20 +15,18 @@ import {
Building,
Hash,
Store,
Bell,
AlertTriangle,
UserPlus,
Settings,
} from 'lucide-react'
import React, { useState } from 'react'
import { toast } from 'sonner'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { UPDATE_SUPPLY_ORDER_STATUS, ASSIGN_LOGISTICS_TO_SUPPLY, FULFILLMENT_RECEIVE_ORDER } from '@/graphql/mutations'
import { ASSIGN_LOGISTICS_TO_SUPPLY, FULFILLMENT_RECEIVE_ORDER } from '@/graphql/mutations'
import {
GET_SUPPLY_ORDERS,
GET_MY_SUPPLIES,

View File

@ -4,7 +4,6 @@ import { Calendar, Package, MapPin, Building2, TrendingUp, AlertTriangle, Dollar
import React, { useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
// Типы данных для товаров ФФ

View File

@ -1,6 +1,6 @@
'use client'
import { Plus, Send, Trash2 } from 'lucide-react'
import { Plus, Send, Trash2, MapPin, Calendar, Phone, Mail, User } from 'lucide-react'
import { useState } from 'react'
import { Badge } from '@/components/ui/badge'

View File

@ -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,42 +18,58 @@ 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)
// 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 [createSupply] = useMutation(CREATE_SUPPLY)
const [updateSupply] = useMutation(UPDATE_SUPPLY)
const [deleteSupply] = useMutation(DELETE_SUPPLY)
const supplies = data?.mySupplies || []
@ -74,68 +80,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,10 +112,6 @@ 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) {
@ -163,9 +122,11 @@ export function SuppliesTab() {
id: originalSupply.id,
name: originalSupply.name,
description: originalSupply.description || '',
price: originalSupply.price.toString(),
pricePerUnit: originalSupply.pricePerUnit ? originalSupply.pricePerUnit.toString() : '',
unit: originalSupply.unit,
imageUrl: originalSupply.imageUrl || '',
isNew: false,
warehouseStock: originalSupply.warehouseStock,
isAvailable: originalSupply.isAvailable,
isEditing: false,
hasChanges: false,
}
@ -174,116 +135,59 @@ export function SuppliesTab() {
)
}
}
// Обновить поле (только цену можно редактировать)
const updateField = (supplyId: string, field: keyof EditableSupply, value: string) => {
if (field !== 'pricePerUnit') {
return // Только цену можно редактировать
}
// Обновить поле
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 {
...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],
},
})
}
}
},
})
} else if (supply.id) {
await updateSupply({
await updateSupplyPrice({
variables: { id: supply.id, input },
update: (cache, { data }) => {
if (data?.updateSupply?.supply) {
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.updateSupply.supply.id ? data.updateSupply.supply : s,
s.id === data.updateSupplyPrice.supply.id ? data.updateSupplyPrice.supply : s,
),
},
})
@ -292,29 +196,22 @@ export function SuppliesTab() {
},
})
}
}
// Удаления теперь происходят сразу в 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 +220,13 @@ 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 +234,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>
@ -381,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"
@ -394,17 +297,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 +310,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 +322,15 @@ 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 +358,61 @@ 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>
)}
</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>
)}
</td>
{/* Действия */}
@ -556,14 +423,9 @@ export function SuppliesTab() {
<Button
size="sm"
onClick={() => {
// Сохраняем только этот расходник если заполнены обязательные поля
if (supply.name.trim() && supply.price) {
saveAllChanges()
} else {
toast.error('Заполните обязательные поля')
}
}}
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 +433,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 +443,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">
Вы действительно хотите удалить расходник &ldquo;{supply.name}&rdquo;? Это действие
необратимо.
</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>

View File

@ -4,17 +4,12 @@ import { useQuery, useMutation } from '@apollo/client'
import {
ArrowLeft,
Building2,
MapPin,
Phone,
Mail,
Star,
Search,
Package,
Plus,
Minus,
ShoppingCart,
Wrench,
Box,
} from 'lucide-react'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
@ -135,14 +130,6 @@ export function CreateConsumablesSupplyPage() {
}).format(amount)
}
const renderStars = (rating: number = 4.5) => {
return Array.from({ length: 5 }, (_, i) => (
<Star
key={i}
className={`h-3 w-3 ${i < Math.floor(rating) ? 'text-yellow-400 fill-current' : 'text-gray-400'}`}
/>
))
}
const updateConsumableQuantity = (productId: string, quantity: number) => {
const product = supplierProducts.find((p: ConsumableProduct) => p.id === productId)
@ -412,7 +399,7 @@ export function CreateConsumablesSupplyPage() {
variant="ghost"
size="sm"
onClick={() => setSelectedSupplier(null)}
className="text-white/70 hover:text-white hover:bg-white/20 text-sm h-8 px-3 flex-shrink-0 rounded-full transition-all duration-300 hover:scale-105"
className="text-white/70 hover:text-white hover:bg-white/20 text-sm h-8 px-3 flex-shrink-0 rounded-full transition-all duration-300"
>
Сбросить
</Button>
@ -440,7 +427,7 @@ export function CreateConsumablesSupplyPage() {
{filteredSuppliers.slice(0, 7).map((supplier: ConsumableSupplier, index) => (
<Card
key={supplier.id}
className={`relative cursor-pointer transition-all duration-300 border flex-shrink-0 rounded-xl overflow-hidden group hover:scale-105 hover:shadow-xl ${
className={`relative cursor-pointer transition-all duration-300 border flex-shrink-0 rounded-xl overflow-hidden group ${
selectedSupplier?.id === supplier.id
? 'bg-gradient-to-br from-orange-500/30 via-orange-400/20 to-orange-500/30 border-orange-400/60 shadow-lg shadow-orange-500/25'
: 'bg-gradient-to-br from-white/10 via-white/5 to-white/10 border-white/20 hover:from-white/20 hover:via-white/10 hover:to-white/20 hover:border-white/40'
@ -494,7 +481,7 @@ export function CreateConsumablesSupplyPage() {
))}
{filteredSuppliers.length > 7 && (
<div
className="flex-shrink-0 flex flex-col items-center justify-center bg-gradient-to-br from-white/10 to-white/5 rounded-xl border border-white/20 text-white/70 hover:text-white transition-all duration-300 hover:scale-105"
className="flex-shrink-0 flex flex-col items-center justify-center bg-gradient-to-br from-white/10 to-white/5 rounded-xl border border-white/20 text-white/70 hover:text-white transition-all duration-300"
style={{ width: 'calc((100% - 48px) / 7)' }}
>
<div className="text-lg font-bold text-purple-300">+{filteredSuppliers.length - 7}</div>
@ -576,7 +563,7 @@ export function CreateConsumablesSupplyPage() {
alt={product.name}
width={100}
height={100}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
className="w-full h-full object-cover"
/>
) : product.mainImage ? (
<Image
@ -584,7 +571,7 @@ export function CreateConsumablesSupplyPage() {
alt={product.name}
width={100}
height={100}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">

View File

@ -4,7 +4,6 @@ import { useQuery, useMutation } from '@apollo/client'
import {
ArrowLeft,
Building2,
Star,
Search,
Package,
Plus,
@ -27,6 +26,9 @@ import { OrganizationAvatar } from '@/components/market/organization-avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
// ВРЕМЕННО ОТКЛЮЧЕНО: импорты для верхней панели - до исправления Apollo ошибки
// import { DatePicker } from '@/components/ui/date-picker'
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { CREATE_SUPPLY_ORDER } from '@/graphql/mutations'
import {
GET_MY_COUNTERPARTIES,
@ -54,6 +56,7 @@ interface GoodsSupplier {
users?: Array<{ id: string; avatar?: string; managerName?: string }>
createdAt: string
rating?: number
market?: string // Принадлежность к рынку согласно rules-complete.md v10.0
}
interface GoodsProduct {
@ -153,7 +156,7 @@ export function CreateSuppliersSupplyPage() {
const [selectedSupplier, setSelectedSupplier] = useState<GoodsSupplier | null>(null)
const [selectedGoods, setSelectedGoods] = useState<SelectedGoodsItem[]>([])
const [searchQuery, setSearchQuery] = useState('')
const [productSearchQuery, setProductSearchQuery] = useState('')
const [productSearchQuery] = useState('')
// Обязательные поля согласно rules2.md 9.7.8
const [deliveryDate, setDeliveryDate] = useState('')
@ -179,30 +182,6 @@ export function CreateSuppliersSupplyPage() {
(GoodsProduct & { selectedQuantity: number; supplierId: string; supplierName: string })[]
>([])
// Состояние для увеличения карточек согласно rules-complete.md 9.2.2.2
const [expandedCard, setExpandedCard] = useState<string | null>(null)
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>(null)
// Функции для увеличения карточек при наведении
const handleCardMouseEnter = (productId: string) => {
if (hoverTimeout) {
clearTimeout(hoverTimeout)
}
const timeout = setTimeout(() => {
setExpandedCard(productId)
}, 2000) // 2 секунды согласно правилам
setHoverTimeout(timeout)
}
const handleCardMouseLeave = () => {
if (hoverTimeout) {
clearTimeout(hoverTimeout)
setHoverTimeout(null)
}
setExpandedCard(null)
}
// Загружаем партнеров-поставщиков согласно rules2.md 13.3
const {
@ -361,6 +340,25 @@ export function CreateSuppliersSupplyPage() {
})
// Моковые логистические компании согласно rules2.md 9.7.7
// Функции для работы с рынками согласно rules-complete.md v10.0
const getMarketLabel = (market?: string) => {
const marketLabels = {
'sadovod': 'Садовод',
'tyak-moscow': 'ТЯК Москва',
'opt-market': 'ОПТ Маркет',
}
return marketLabels[market as keyof typeof marketLabels] || market
}
const getMarketBadgeStyle = (market?: string) => {
const styles = {
'sadovod': 'bg-green-500/20 text-green-300 border-green-500/30',
'tyak-moscow': 'bg-blue-500/20 text-blue-300 border-blue-500/30',
'opt-market': 'bg-purple-500/20 text-purple-300 border-purple-500/30',
}
return styles[market as keyof typeof styles] || 'bg-gray-500/20 text-gray-300 border-gray-500/30'
}
const logisticsCompanies: LogisticsCompany[] = [
{ id: 'express', name: 'Экспресс доставка', estimatedCost: 2500, deliveryDays: 1, type: 'EXPRESS' },
{ id: 'standard', name: 'Стандартная доставка', estimatedCost: 1200, deliveryDays: 3, type: 'STANDARD' },
@ -386,11 +384,7 @@ export function CreateSuppliersSupplyPage() {
}))
}
const updateProductQuantity = (productId: string, delta: number): void => {
const currentQuantity = getProductQuantity(productId)
const newQuantity = currentQuantity + delta
setProductQuantity(productId, newQuantity)
}
// Removed unused updateProductQuantity function
// Добавление товара в корзину из карточки с заданным количеством
const addToCart = (product: GoodsProduct) => {
@ -445,11 +439,7 @@ export function CreateSuppliersSupplyPage() {
setProductQuantity(product.id, 0)
}
// Открытие модального окна для детального добавления
const openAddModal = (product: GoodsProduct) => {
setSelectedProductForModal(product)
setIsModalOpen(true)
}
// Removed unused openAddModal function
// Функции для работы с рецептурой
const initializeProductRecipe = (productId: string) => {
@ -704,39 +694,36 @@ export function CreateSuppliersSupplyPage() {
Назад
</Button>
<div className="h-4 w-px bg-white/20"></div>
<div className="p-2 bg-green-400/10 rounded-lg border border-green-400/20">
<Building2 className="h-4 w-4 text-green-400" />
<Building2 className="h-5 w-5 text-blue-400" />
<h2 className="text-lg font-semibold text-white">Поставщики</h2>
</div>
<div>
<h2 className="text-base font-semibold text-white">Поставщики товаров</h2>
<Badge className="bg-purple-500/20 text-purple-300 border border-purple-500/30 text-xs font-medium mt-0.5">
Создание поставки
</Badge>
</div>
</div>
<div className="flex-1 max-w-sm">
<div className="w-64">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-4 w-4" />
<Input
placeholder="Поиск..."
placeholder="Поиск поставщиков..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/50 pl-10 h-9 text-sm transition-all duration-200 focus:border-white/20"
/>
</div>
</div>
</div>
{/* Кнопка поиска в маркете */}
{allCounterparties.length === 0 && (
<div className="mt-4">
<Button
variant="outline"
size="sm"
onClick={() => router.push('/market')}
className="glass-secondary hover:text-white/90 transition-all duration-200 mt-2 w-full"
className="glass-secondary hover:text-white/90 transition-all duration-200"
>
<Building2 className="h-3 w-3 mr-2" />
Найти поставщиков в маркете
</Button>
</div>
)}
</div>
</div>
{/* Список поставщиков согласно visual-design-rules.md */}
<div className="flex-1 min-h-0">
@ -790,35 +777,20 @@ export function CreateSuppliersSupplyPage() {
<OrganizationAvatar organization={supplier} size="sm" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="text-white font-medium text-sm truncate group-hover:text-white transition-colors">
{supplier.name || supplier.fullName}
</h4>
{supplier.rating && (
<div className="flex items-center gap-1 bg-yellow-400/10 px-2 py-0.5 rounded-full">
<Star className="h-3 w-3 text-yellow-400 fill-current" />
<span className="text-yellow-300 text-xs font-medium">{supplier.rating}</span>
</div>
)}
</div>
<div className="flex items-center gap-2 mb-1">
<div className="flex items-center gap-2 mt-1">
<p className="text-white/60 text-xs font-mono">ИНН: {supplier.inn}</p>
<Badge
className={`text-xs font-medium ${
supplier.type === 'WHOLESALE'
? 'bg-green-500/20 text-green-300 border border-green-500/30'
: 'bg-yellow-500/20 text-yellow-300 border border-yellow-500/30'
}`}
>
{supplier.type === 'WHOLESALE' ? 'Поставщик' : supplier.type}
{supplier.market && (
<Badge className={`text-xs font-medium border ${getMarketBadgeStyle(supplier.market)}`}>
{getMarketLabel(supplier.market)}
</Badge>
</div>
{supplier.address && (
<p className="text-white/50 text-xs line-clamp-1">{supplier.address}</p>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
@ -826,63 +798,14 @@ export function CreateSuppliersSupplyPage() {
</div>
</div>
{/* БЛОК 2: КАРТОЧКИ ТОВАРОВ - новый блок согласно rules-complete.md 9.2.2 */}
<div
className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-shrink-0 flex flex-col"
style={{ height: '160px' }}
>
<div className="p-4 border-b border-white/10 flex-shrink-0">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-400/10 rounded-lg border border-blue-400/20">
<Package className="h-4 w-4 text-blue-400" />
</div>
<div>
<h3 className="text-base font-semibold text-white">
{selectedSupplier
? `Товары ${selectedSupplier.name || selectedSupplier.fullName}`
: 'Карточки товаров'}
</h3>
<p className="text-white/60 text-sm">Компактные карточки для быстрого выбора</p>
</div>
</div>
</div>
</div>
<div className="flex-1 overflow-hidden">
{!selectedSupplier ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Package className="h-8 w-8 text-blue-400/50 mx-auto mb-2" />
<p className="text-white/60 text-sm">Выберите поставщика</p>
</div>
</div>
) : products.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Package className="h-8 w-8 text-white/40 mx-auto mb-2" />
<p className="text-white/60 text-sm">Нет товаров</p>
</div>
</div>
) : (
<div className="flex gap-3 overflow-x-auto p-4 h-full" style={{ scrollbarWidth: 'thin' }}>
{products.map((product: GoodsProduct) => {
const isExpanded = expandedCard === product.id
{/* БЛОК 2: КАРТОЧКИ ТОВАРОВ */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-shrink-0">
<div className="flex gap-3 overflow-x-auto p-4" style={{ scrollbarWidth: 'thin' }}>
{selectedSupplier && products.length > 0 && products.map((product: GoodsProduct) => {
return (
<div
key={product.id}
className={`relative flex-shrink-0 bg-white/5 rounded-lg overflow-hidden border cursor-pointer transition-all duration-300 group ${
isExpanded
? 'w-80 h-112 border-white/50 shadow-2xl z-50 scale-105'
: 'w-20 h-28 border-white/10 hover:border-white/30'
}`}
style={{
transform: isExpanded ? 'scale(4)' : 'scale(1)',
zIndex: isExpanded ? 50 : 1,
transformOrigin: 'center center',
}}
onMouseEnter={() => handleCardMouseEnter(product.id)}
onMouseLeave={handleCardMouseLeave}
className="relative flex-shrink-0 bg-white/5 rounded-lg overflow-hidden border cursor-pointer transition-all duration-300 group w-20 h-28 border-white/10 hover:border-white/30"
onClick={() => {
// Добавляем товар в детальный каталог (блок 3)
if (!allSelectedProducts.find((p) => p.id === product.id)) {
@ -898,62 +821,85 @@ export function CreateSuppliersSupplyPage() {
}
}}
>
{isExpanded ? (
<div className="p-3 space-y-2 bg-white/10 backdrop-blur-xl h-full">
{product.mainImage ? (
<Image
src={product.mainImage}
alt={product.name}
width={60}
height={60}
className="w-15 h-15 object-cover rounded mx-auto"
/>
) : (
<div className="w-15 h-15 bg-white/5 rounded flex items-center justify-center mx-auto">
<Package className="h-8 w-8 text-white/40" />
</div>
)}
<div className="text-center space-y-1">
<h4 className="text-white font-semibold text-sm truncate">{product.name}</h4>
<p className="text-green-400 font-bold text-base">
{product.price.toLocaleString('ru-RU')}
</p>
{product.category && <p className="text-blue-300 text-xs">{product.category.name}</p>}
{product.quantity !== undefined && (
<p className="text-white/60 text-xs">Доступно: {product.quantity}</p>
)}
<p className="text-white/50 text-xs font-mono">Артикул: {product.article}</p>
</div>
</div>
) : (
<>
{product.mainImage ? (
<Image
src={product.mainImage}
alt={product.name}
width={80}
height={112}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-6 w-6 text-white/40" />
</div>
)}
</>
)}
</div>
)
})}
</div>
)}
</div>
</div>
{/* БЛОК 3: ТОВАРЫ ПОСТАВЩИКА - детальный каталог согласно rules-complete.md 9.2 */}
{/* БЛОК 3: ТОВАРЫ ПОСТАВЩИКА - детальный каталог согласно rules-complete.md 9.2.3 */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-1 min-h-0 flex flex-col">
<div className="p-6 border-b border-white/10 flex-shrink-0">
<div className="flex items-center justify-between gap-6">
{/* ВРЕМЕННО ОТКЛЮЧЕНО: Верхняя панель согласно правилам 9.2.3.1 - до исправления Apollo ошибки
{!counterpartiesLoading && (
<div className="flex items-center gap-4 p-4 bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl mb-4">
<DatePicker
placeholder="Дата поставки"
value={deliveryDate}
onChange={setDeliveryDate}
className="min-w-[140px]"
/>
<Select value={selectedFulfillment} onValueChange={setSelectedFulfillment}>
<SelectTrigger className="glass-input min-w-[200px]">
<SelectValue placeholder="Выберите фулфилмент" />
</SelectTrigger>
<SelectContent>
{allCounterparties && allCounterparties.length > 0 ? (
allCounterparties
.filter((partner) => partner.type === 'FULFILLMENT')
.map((fulfillment) => (
<SelectItem key={fulfillment.id} value={fulfillment.id}>
{fulfillment.name || fulfillment.fullName}
</SelectItem>
))
) : (
<SelectItem value="" disabled>
Нет доступных фулфилмент-центров
</SelectItem>
)}
</SelectContent>
</Select>
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск товаров..."
value={productSearchQuery}
onChange={(e) => setProductSearchQuery(e.target.value)}
className="pl-10 glass-input"
/>
</div>
</div>
)}
{counterpartiesLoading && (
<div className="flex items-center justify-center p-4 bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl mb-4">
<div className="text-white/60 text-sm">Загрузка партнеров...</div>
</div>
)}
{counterpartiesError && (
<div className="flex items-center justify-center p-4 bg-red-500/10 backdrop-blur-xl border border-red-500/20 rounded-2xl mb-4">
<div className="text-red-300 text-sm">
Ошибка загрузки партнеров: {counterpartiesError.message}
</div>
</div>
)}
*/}
{/* Заголовок каталога */}
<div className="px-6 py-4 border-b border-white/10 flex-shrink-0">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-400/10 rounded-lg border border-blue-400/20">
<Package className="h-6 w-6 text-blue-400" />
@ -962,23 +908,9 @@ export function CreateSuppliersSupplyPage() {
<h3 className="text-xl font-semibold text-white">
Детальный каталог ({allSelectedProducts.length} товаров)
</h3>
<p className="text-white/60 text-sm mt-1">Товары из блока карточек для детального управления</p>
<p className="text-white/60 text-sm mt-1">Товары для детального управления поставкой</p>
</div>
</div>
{selectedSupplier && (
<div className="flex-1 max-w-sm">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-4 w-4" />
<Input
placeholder="Поиск товаров..."
value={productSearchQuery}
onChange={(e) => setProductSearchQuery(e.target.value)}
className="glass-input text-white placeholder:text-white/50 pl-10 h-10 transition-all duration-200 focus-visible:ring-ring/50"
/>
</div>
</div>
)}
</div>
</div>
<div className="flex-1 overflow-y-auto p-6">
@ -991,7 +923,7 @@ export function CreateSuppliersSupplyPage() {
<div>
<h4 className="text-xl font-medium text-white mb-2">Детальный каталог пуст</h4>
<p className="text-white/60 max-w-sm mx-auto">
Добавьте товары из блока карточек выше для детального управления
Добавьте товары
</p>
</div>
</div>
@ -1289,7 +1221,6 @@ export function CreateSuppliersSupplyPage() {
const quantity = getProductQuantity(product.id)
const recipeCost = calculateRecipeCost(product.id)
const productTotal = product.price * quantity
const totalRecipePrice = productTotal + recipeCost.total
return (
<>

View File

@ -6,7 +6,7 @@ import React, { useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { SelectedCard, WildberriesCard } from '@/types/supplies'
import { SelectedCard } from '@/types/supplies'
import { WBProductCards } from './wb-product-cards'
@ -74,7 +74,6 @@ const mockWholesalers: Wholesaler[] = [
export function CreateSupplyForm({ onClose, onSupplyCreated }: CreateSupplyFormProps) {
const [selectedVariant, setSelectedVariant] = useState<'cards' | 'wholesaler' | null>(null)
const [selectedWholesaler, setSelectedWholesaler] = useState<Wholesaler | null>(null)
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([])
const renderStars = (rating: number) => {
return Array.from({ length: 5 }, (_, i) => (
@ -86,7 +85,6 @@ export function CreateSupplyForm({ onClose, onSupplyCreated }: CreateSupplyFormP
}
const handleCardsComplete = (cards: SelectedCard[]) => {
setSelectedCards(cards)
console.warn('Карточки товаров выбраны:', cards)
// TODO: Здесь будет создание поставки с данными карточек
onSupplyCreated()
@ -164,7 +162,7 @@ export function CreateSupplyForm({ onClose, onSupplyCreated }: CreateSupplyFormP
{mockWholesalers.map((wholesaler) => (
<Card
key={wholesaler.id}
className="bg-white/10 backdrop-blur border-white/20 p-6 cursor-pointer transition-all hover:bg-white/15 hover:border-white/30 hover:scale-105"
className="bg-white/10 backdrop-blur border-white/20 p-6 cursor-pointer transition-all hover:bg-white/15 hover:border-white/30"
onClick={() => setSelectedWholesaler(wholesaler)}
>
<div className="space-y-4">

View File

@ -50,7 +50,7 @@ const CreateSupplyPage = React.memo(() => {
const fulfillmentOrgs = useMemo(() =>
(counterpartiesData?.myCounterparties || []).filter(
(org: Organization) => org.type === 'FULFILLMENT',
), [counterpartiesData?.myCounterparties]
), [counterpartiesData?.myCounterparties],
)
const formatCurrency = useCallback((amount: number) => {

View File

@ -25,12 +25,12 @@ export function ProductCard({ product, selectedQuantity, onQuantityChange, forma
}
return (
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden group hover:bg-white/15 hover:border-white/30 transition-all duration-300 hover:scale-105 hover:shadow-2xl">
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden group hover:bg-white/15 hover:border-white/30 transition-all duration-300">
<div className="aspect-square relative bg-white/5 overflow-hidden">
<img
src={product.mainImage || '/api/placeholder/400/400'}
alt={product.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
className="w-full h-full object-cover"
/>
{/* Количество в наличии */}

View File

@ -16,7 +16,7 @@ interface SupplierCardProps {
export function SupplierCard({ supplier, onClick }: SupplierCardProps) {
return (
<Card
className="bg-white/10 backdrop-blur border-white/20 p-4 cursor-pointer transition-all hover:bg-white/15 hover:border-white/30 hover:scale-[1.02]"
className="bg-white/10 backdrop-blur border-white/20 p-4 cursor-pointer transition-all hover:bg-white/15 hover:border-white/30"
onClick={onClick}
>
<div className="space-y-3">

View File

@ -1,7 +1,24 @@
'use client'
import { useQuery, useMutation } from '@apollo/client'
import { format } from 'date-fns'
import { ru } from 'date-fns/locale'
import {
Search,
Plus,
Minus,
ShoppingCart,
Calendar as CalendarIcon,
Package,
ArrowLeft,
Check,
Eye,
ChevronLeft,
ChevronRight,
} from 'lucide-react'
import React, { useState, useEffect } from 'react'
import DatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'
import { toast } from 'sonner'
import { Badge } from '@/components/ui/badge'
@ -9,43 +26,16 @@ import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { ProductCardSkeletonGrid } from '@/components/ui/product-card-skeleton'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import 'react-datepicker/dist/react-datepicker.css'
import { CREATE_WILDBERRIES_SUPPLY } from '@/graphql/mutations'
import { GET_MY_COUNTERPARTIES, GET_COUNTERPARTY_SERVICES, GET_COUNTERPARTY_SUPPLIES } from '@/graphql/queries'
import { useAuth } from '@/hooks/useAuth'
import { useSidebar } from '@/hooks/useSidebar'
import {
Search,
Plus,
Minus,
ShoppingCart,
Calendar as CalendarIcon,
Phone,
User,
MapPin,
Package,
Wrench,
ArrowLeft,
Check,
Eye,
ChevronLeft,
ChevronRight,
} from 'lucide-react'
import { apolloClient } from '@/lib/apollo-client'
import { WildberriesService } from '@/services/wildberries-service'
import { useQuery, useMutation } from '@apollo/client'
import { format } from 'date-fns'
import { ru } from 'date-fns/locale'
import { SelectedCard, FulfillmentService, ConsumableService, WildberriesCard } from '@/types/supplies'
import { SelectedCard, WildberriesCard } from '@/types/supplies'
interface Organization {
id: string
@ -64,7 +54,7 @@ interface WBProductCardsProps {
}
export function WBProductCards({
onBack,
_onBack, // eslint-disable-line @typescript-eslint/no-unused-vars
onComplete,
showSummary: externalShowSummary,
setShowSummary: externalSetShowSummary,
@ -88,7 +78,6 @@ export function WBProductCards({
const actualShowSummary = externalShowSummary !== undefined ? externalShowSummary : showSummary
const actualSetShowSummary = externalSetShowSummary || setShowSummary
const [globalDeliveryDate, setGlobalDeliveryDate] = useState<Date | undefined>(undefined)
const [fulfillmentServices, setFulfillmentServices] = useState<FulfillmentService[]>([])
const [organizationServices, setOrganizationServices] = useState<{
[orgId: string]: Array<{ id: string; name: string; description?: string; price: number }>
}>({})
@ -193,13 +182,6 @@ export function WBProductCards({
},
})
// Данные рынков можно будет загружать через GraphQL в будущем
const markets = [
{ value: 'sadovod', label: 'Садовод' },
{ value: 'luzhniki', label: 'Лужники' },
{ value: 'tishinka', label: 'Тишинка' },
{ value: 'food-city', label: 'Фуд Сити' },
]
// Загружаем карточки из GraphQL запроса
useEffect(() => {
@ -1007,12 +989,11 @@ export function WBProductCards({
{wbCards.map((card) => {
const selectedQuantity = getSelectedQuantity(card)
const isSelected = selectedQuantity > 0
const selectedCard = actualSelectedCards.find((sc) => sc.card.nmID === card.nmID)
return (
<Card
key={card.nmID}
className={`bg-white/10 backdrop-blur border-white/20 transition-all hover:scale-105 hover:shadow-2xl group ${isSelected ? 'ring-2 ring-purple-500/50 bg-purple-500/10' : ''} relative overflow-hidden`}
className={`bg-white/10 backdrop-blur border-white/20 transition-all group ${isSelected ? 'ring-2 ring-purple-500/50 bg-purple-500/10' : ''} relative overflow-hidden`}
>
<div className="p-2 space-y-2">
{/* Изображение и основная информация */}
@ -1022,7 +1003,7 @@ export function WBProductCards({
<img
src={WildberriesService.getCardImage(card, 'c516x688') || '/api/placeholder/300/300'}
alt={card.title}
className="w-full h-full object-cover cursor-pointer group-hover:scale-110 transition-transform duration-500"
className="w-full h-full object-cover cursor-pointer"
onClick={() => handleCardClick(card)}
/>

View File

@ -49,6 +49,7 @@ interface Product {
isActive: boolean
createdAt: string
updatedAt: string
organization: { id: string; market?: string }
}
interface ProductCardProps {
@ -57,6 +58,30 @@ interface ProductCardProps {
onDeleted: () => void
}
// Функция для отображения бэйджа рынка согласно правилам системы
const getMarketBadge = (market?: string) => {
if (!market) return null
const marketStyles = {
sadovod: 'bg-green-500/20 text-green-300 border-green-500/30',
'tyak-moscow': 'bg-blue-500/20 text-blue-300 border-blue-500/30',
}
const marketLabels = {
sadovod: 'Садовод',
'tyak-moscow': 'ТЯК Москва',
}
const style = marketStyles[market as keyof typeof marketStyles] || 'bg-gray-500/20 text-gray-300 border-gray-500/30'
const label = marketLabels[market as keyof typeof marketLabels] || market
return (
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium border ${style}`}>
{label}
</span>
)
}
export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) {
const [deleteProduct, { loading: deleting }] = useMutation(DELETE_PRODUCT)
const [imageDialogOpen, setImageDialogOpen] = useState(false)
@ -103,7 +128,7 @@ export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) {
}
return (
<Card className="glass-card group relative overflow-hidden transition-all duration-300 hover:scale-[1.02] hover:shadow-xl hover:shadow-purple-500/20">
<Card className="glass-card group relative overflow-hidden transition-all duration-300">
{/* Изображение товара */}
<div className="relative h-48 bg-white/5 overflow-hidden flex items-center justify-center">
{product.mainImage || product.images[0] ? (
@ -115,7 +140,7 @@ export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) {
alt={product.name}
width={300}
height={200}
className="w-full h-full object-contain transition-transform duration-300 group-hover:scale-110"
className="w-full h-full object-contain"
/>
</div>
</DialogTrigger>
@ -248,6 +273,9 @@ export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) {
{product.type === 'PRODUCT' ? 'Товар' : 'Расходник'}
</Badge>
{/* Рынок */}
{getMarketBadge(product.organization?.market)}
{/* Категория */}
{product.category && (
<Badge variant="outline" className="glass-secondary text-white/60 border-white/20 text-xs">

View File

@ -38,6 +38,7 @@ interface Product {
images: string[]
mainImage: string
isActive: boolean
organization?: { id: string; market?: string }
}
interface ProductFormProps {
@ -46,6 +47,7 @@ interface ProductFormProps {
onCancel: () => void
}
export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
const [formData, setFormData] = useState({
name: product?.name || '',

View File

@ -41,8 +41,34 @@ interface Product {
isActive: boolean
createdAt: string
updatedAt: string
organization: { id: string; market?: string }
}
// Функция для отображения бэйджа рынка согласно правилам системы
const getMarketBadge = (market?: string) => {
if (!market) return null
const marketStyles = {
sadovod: 'bg-green-500/20 text-green-300 border-green-500/30',
'tyak-moscow': 'bg-blue-500/20 text-blue-300 border-blue-500/30',
}
const marketLabels = {
sadovod: 'Садовод',
'tyak-moscow': 'ТЯК Москва',
}
const style = marketStyles[market as keyof typeof marketStyles] || 'bg-gray-500/20 text-gray-300 border-gray-500/30'
const label = marketLabels[market as keyof typeof marketLabels] || market
return (
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium border ${style}`}>
{label}
</span>
)
}
export function WarehouseDashboard() {
const { getSidebarMargin } = useSidebar()
const [isDialogOpen, setIsDialogOpen] = useState(false)
@ -57,14 +83,17 @@ export function WarehouseDashboard() {
const products: Product[] = data?.myProducts || []
// Фильтрация товаров по поисковому запросу
const filteredProducts = products.filter(
(product) =>
const filteredProducts = products.filter((product) => {
const matchesSearch = !searchQuery || (
product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.article.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.category?.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.brand?.toLowerCase().includes(searchQuery.toLowerCase()),
product.brand?.toLowerCase().includes(searchQuery.toLowerCase())
)
return matchesSearch
})
const handleCreateProduct = () => {
setEditingProduct(null)
setIsDialogOpen(true)
@ -118,13 +147,14 @@ export function WarehouseDashboard() {
<div className="relative max-w-md">
<Input
type="text"
placeholder="Поиск по названию, артикулу, категории..."
placeholder="Поиск по названию, артикулу, рынку, категории..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="glass-input text-white placeholder:text-white/50 h-10"
/>
</div>
{/* Переключатель режимов отображения */}
<div className="flex border border-white/10 rounded-lg overflow-hidden">
<Button
@ -234,7 +264,7 @@ export function WarehouseDashboard() {
<div className="col-span-2">Название</div>
<div className="col-span-1">Артикул</div>
<div className="col-span-1">Тип</div>
<div className="col-span-1">Категория</div>
<div className="col-span-1">Рынок</div>
<div className="col-span-1">Цена</div>
<div className="col-span-1">Остаток</div>
<div className="col-span-1">Заказано</div>
@ -276,7 +306,9 @@ export function WarehouseDashboard() {
{product.type === 'PRODUCT' ? 'Товар' : 'Расходник'}
</span>
</div>
<div className="col-span-1 text-white/70 text-sm">{product.category?.name || 'Нет'}</div>
<div className="col-span-1 text-white/70 text-sm">
{getMarketBadge(product.organization?.market) || <span className="text-white/40 text-xs">Не указан</span>}
</div>
<div className="col-span-1 text-white text-sm font-medium">
{new Intl.NumberFormat('ru-RU', {
style: 'currency',

View File

@ -212,6 +212,7 @@ export const UPDATE_USER_PROFILE = gql`
ogrn
ogrnDate
type
market
status
actualityDate
registrationDate
@ -622,62 +623,30 @@ export const DELETE_SERVICE = gql`
}
`
// Мутации для расходников
export const CREATE_SUPPLY = gql`
mutation CreateSupply($input: SupplyInput!) {
createSupply(input: $input) {
// Мутации для расходников - только обновление цены разрешено
export const UPDATE_SUPPLY_PRICE = gql`
mutation UpdateSupplyPrice($id: ID!, $input: UpdateSupplyPriceInput!) {
updateSupplyPrice(id: $id, input: $input) {
success
message
supply {
id
name
description
price
quantity
pricePerUnit
unit
category
status
date
supplier
minStock
currentStock
imageUrl
warehouseStock
isAvailable
warehouseConsumableId
createdAt
updatedAt
}
}
}
`
export const UPDATE_SUPPLY = gql`
mutation UpdateSupply($id: ID!, $input: SupplyInput!) {
updateSupply(id: $id, input: $input) {
success
message
supply {
organization {
id
name
description
price
quantity
unit
category
status
date
supplier
minStock
currentStock
imageUrl
createdAt
updatedAt
}
}
}
`
export const DELETE_SUPPLY = gql`
mutation DeleteSupply($id: ID!) {
deleteSupply(id: $id)
}
`
@ -776,6 +745,11 @@ export const CREATE_LOGISTICS = gql`
description
createdAt
updatedAt
organization {
id
name
fullName
}
}
}
}
@ -795,6 +769,11 @@ export const UPDATE_LOGISTICS = gql`
description
createdAt
updatedAt
organization {
id
name
fullName
}
}
}
}
@ -841,6 +820,10 @@ export const CREATE_PRODUCT = gql`
isActive
createdAt
updatedAt
organization {
id
market
}
}
}
}
@ -880,6 +863,10 @@ export const UPDATE_PRODUCT = gql`
isActive
createdAt
updatedAt
organization {
id
market
}
}
}
}

View File

@ -52,6 +52,7 @@ export const GET_ME = gql`
ogrn
ogrnDate
type
market
status
actualityDate
registrationDate
@ -104,19 +105,32 @@ export const GET_MY_SUPPLIES = gql`
id
name
description
price
quantity
pricePerUnit
unit
category
status
date
supplier
minStock
currentStock
usedStock
imageUrl
warehouseStock
isAvailable
warehouseConsumableId
createdAt
updatedAt
organization {
id
name
}
}
}
`
// Новый запрос для получения доступных расходников для рецептур селлеров
export const GET_AVAILABLE_SUPPLIES_FOR_RECIPE = gql`
query GetAvailableSuppliesForRecipe {
getAvailableSuppliesForRecipe {
id
name
pricePerUnit
unit
imageUrl
warehouseStock
}
}
`
@ -247,6 +261,10 @@ export const GET_MY_PRODUCTS = gql`
isActive
createdAt
updatedAt
organization {
id
market
}
}
}
`
@ -321,6 +339,7 @@ export const GET_MY_COUNTERPARTIES = gql`
managementName
type
address
market
phones
emails
createdAt

View File

@ -687,28 +687,10 @@ export const resolvers = {
throw new GraphQLError('У пользователя нет организации')
}
// Получаем заказы поставок, где фулфилмент является получателем,
// но НЕ создателем (т.е. селлеры заказали расходники для фулфилмента)
const sellerSupplyOrders = await prisma.supplyOrder.findMany({
where: {
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
organizationId: { not: currentUser.organization.id }, // Создатель - НЕ мы
status: 'DELIVERED', // Только доставленные
},
include: {
organization: true,
partner: true,
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
},
})
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
return [] // Только фулфилменты имеют расходники
}
// Получаем ВСЕ расходники из таблицы supply для фулфилмента
const allSupplies = await prisma.supply.findMany({
@ -717,52 +699,39 @@ export const resolvers = {
orderBy: { createdAt: 'desc' },
})
// Получаем все заказы фулфилмента для себя (чтобы исключить их расходники)
const fulfillmentOwnOrders = await prisma.supplyOrder.findMany({
where: {
organizationId: currentUser.organization.id, // Созданы фулфилментом
fulfillmentCenterId: currentUser.organization.id, // Для себя
status: 'DELIVERED',
},
include: {
items: {
include: {
product: true,
},
},
},
})
// Преобразуем старую структуру в новую согласно GraphQL схеме
const transformedSupplies = allSupplies.map((supply) => ({
id: supply.id,
name: supply.name,
description: supply.description,
pricePerUnit: supply.price ? parseFloat(supply.price.toString()) : null, // Конвертируем Decimal в Number
unit: supply.unit || 'шт', // Единица измерения
imageUrl: supply.imageUrl,
warehouseStock: supply.currentStock || 0, // Остаток на складе
isAvailable: (supply.currentStock || 0) > 0, // Есть ли в наличии
warehouseConsumableId: supply.id, // Связь со складом (пока используем тот же ID)
createdAt: supply.createdAt,
updatedAt: supply.updatedAt,
organization: supply.organization,
}))
// Создаем набор названий товаров из заказов фулфилмента для себя
const fulfillmentProductNames = new Set(
fulfillmentOwnOrders.flatMap((order) => order.items.map((item) => item.product.name)),
)
// Фильтруем расходники: исключаем те, что созданы заказами фулфилмента для себя
const sellerSupplies = allSupplies.filter((supply) => {
// Если расходник соответствует товару из заказа фулфилмента для себя,
// то это расходник фулфилмента, а не селлера
return !fulfillmentProductNames.has(supply.name)
})
// Логирование для отладки
console.warn('🔥🔥🔥 SELLER SUPPLIES RESOLVER CALLED 🔥🔥🔥')
console.warn('📊 Расходники селлеров:', {
console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', {
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
allSuppliesCount: allSupplies.length,
fulfillmentOwnOrdersCount: fulfillmentOwnOrders.length,
fulfillmentProductNames: Array.from(fulfillmentProductNames),
filteredSellerSuppliesCount: sellerSupplies.length,
sellerOrdersCount: sellerSupplyOrders.length,
suppliesCount: transformedSupplies.length,
supplies: transformedSupplies.map((s) => ({
id: s.id,
name: s.name,
pricePerUnit: s.pricePerUnit,
warehouseStock: s.warehouseStock,
isAvailable: s.isAvailable,
})),
})
// Возвращаем только расходники селлеров (исключая расходники фулфилмента)
return sellerSupplies
return transformedSupplies
},
// Расходники фулфилмента (материалы для работы фулфилмента)
myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => {
// Доступные расходники для рецептур селлеров (только с ценой и в наличии)
getAvailableSuppliesForRecipe: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
@ -778,83 +747,90 @@ export const resolvers = {
throw new GraphQLError('У пользователя нет организации')
}
// TypeScript assertion - мы знаем что organization не null после проверки выше
const organization = currentUser.organization
// Селлеры могут получать расходники от своих фулфилмент-партнеров
if (currentUser.organization.type !== 'SELLER') {
return [] // Только селлеры используют рецептуры
}
// Получаем заказы поставок, созданные этим фулфилмент-центром для себя
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
// TODO: В будущем здесь будет логика получения расходников от партнерских фулфилментов
// Пока возвращаем пустой массив, так как эта функциональность еще разрабатывается
console.warn('🔥 getAvailableSuppliesForRecipe called for seller:', {
sellerId: currentUser.organization.id,
sellerName: currentUser.organization.name,
})
return []
},
// Расходники фулфилмента из склада (новая архитектура - синхронизация со склада)
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('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
console.warn('👤 Current user:', {
id: currentUser?.id,
phone: currentUser?.phone,
organizationId: currentUser?.organizationId,
organizationType: currentUser?.organization?.type,
organizationName: currentUser?.organization?.name,
})
if (!currentUser?.organization) {
console.warn('❌ No organization for user')
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
console.warn('❌ User organization is not FULFILLMENT:', currentUser.organization.type)
throw new GraphQLError('Доступ только для фулфилмент центров')
}
// Получаем расходники фулфилмента из таблицы Supply
const supplies = await prisma.supply.findMany({
where: {
organizationId: organization.id, // Создали мы
fulfillmentCenterId: organization.id, // Получатель - мы
status: {
in: ['PENDING', 'CONFIRMED', 'IN_TRANSIT', 'DELIVERED'], // Все статусы
},
organizationId: currentUser.organization.id,
type: 'FULFILLMENT_CONSUMABLES', // Только расходники фулфилмента
},
include: {
partner: true,
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
organization: true,
},
orderBy: { createdAt: 'desc' },
})
// Преобразуем заказы поставок в формат supply для единообразия
const fulfillmentSupplies = fulfillmentSupplyOrders.flatMap((order) =>
order.items.map((item) => ({
id: `fulfillment-order-${order.id}-${item.id}`,
name: item.product.name,
description: item.product.description || `Расходники от ${order.partner.name}`,
price: item.price,
quantity: item.quantity,
unit: 'шт',
category: item.product.category?.name || 'Расходники фулфилмента',
status:
order.status === 'PENDING'
? 'planned'
: order.status === 'CONFIRMED'
? 'confirmed'
: order.status === 'IN_TRANSIT'
? 'in-transit'
: order.status === 'DELIVERED'
? 'in-stock'
: 'planned',
date: order.createdAt,
supplier: order.partner.name || order.partner.fullName || 'Не указан',
minStock: Math.round(item.quantity * 0.1),
currentStock: order.status === 'DELIVERED' ? item.quantity : 0,
usedStock: 0, // TODO: Подсчитывать реальное использование
imageUrl: null,
createdAt: order.createdAt,
updatedAt: order.updatedAt,
organizationId: organization.id,
organization: organization,
shippedQuantity: 0,
})),
)
// Логирование для отладки
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED 🔥🔥🔥')
console.warn('📊 Расходники фулфилмента:', {
organizationId: organization.id,
organizationType: organization.type,
fulfillmentOrdersCount: fulfillmentSupplyOrders.length,
fulfillmentSuppliesCount: fulfillmentSupplies.length,
fulfillmentOrders: fulfillmentSupplyOrders.map((o) => ({
id: o.id,
supplierName: o.partner.name,
status: o.status,
itemsCount: o.items.length,
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥')
console.warn('📊 Расходники фулфилмента из склада:', {
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
suppliesCount: supplies.length,
supplies: supplies.map((s) => ({
id: s.id,
name: s.name,
type: s.type,
status: s.status,
currentStock: s.currentStock,
quantity: s.quantity,
})),
})
return fulfillmentSupplies
// Преобразуем в формат для фронтенда
return supplies.map((supply) => ({
...supply,
price: supply.price ? parseFloat(supply.price.toString()) : 0,
shippedQuantity: 0, // Добавляем для совместимости
}))
},
// Заказы поставок расходников
@ -1411,12 +1387,6 @@ export const resolvers = {
// Мои товары и расходники (для поставщиков)
myProducts: async (_: unknown, __: unknown, context: Context) => {
console.warn('🔍 MY_PRODUCTS RESOLVER - ВЫЗВАН:', {
hasUser: !!context.user,
userId: context.user?.id,
timestamp: new Date().toISOString(),
})
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
@ -1428,23 +1398,12 @@ export const resolvers = {
include: { organization: true },
})
console.warn('👤 ПОЛЬЗОВАТЕЛЬ НАЙДЕН:', {
userId: currentUser?.id,
hasOrganization: !!currentUser?.organization,
organizationType: currentUser?.organization?.type,
organizationName: currentUser?.organization?.name,
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это поставщик
if (currentUser.organization.type !== 'WHOLESALE') {
console.warn('❌ ДОСТУП ЗАПРЕЩЕН - НЕ ПОСТАВЩИК:', {
actualType: currentUser.organization.type,
requiredType: 'WHOLESALE',
})
throw new GraphQLError('Товары доступны только для поставщиков')
}
@ -2586,6 +2545,7 @@ export const resolvers = {
bik?: string
accountNumber?: string
corrAccount?: string
market?: string
}
},
context: Context,
@ -2636,6 +2596,7 @@ export const resolvers = {
emails?: object
managementName?: string
managementPost?: string
market?: string
} = {}
// Название организации больше не обновляется через профиль
@ -2651,6 +2612,11 @@ export const resolvers = {
updateData.emails = [{ value: input.email, type: 'main' }]
}
// Обновляем рынок для поставщиков
if (input.market !== undefined) {
updateData.market = input.market === 'none' ? null : input.market
}
// Сохраняем дополнительные контакты в custom полях
// Пока добавим их как дополнительные JSON поля
const customContacts: {
@ -3639,23 +3605,13 @@ export const resolvers = {
}
},
// Создать расходник
createSupply: async (
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
updateSupplyPrice: async (
_: unknown,
args: {
id: string
input: {
name: string
description?: string
price: number
quantity: number
unit: string
category: string
status: string
date: string
supplier: string
minStock: number
currentStock: number
imageUrl?: string
pricePerUnit?: number | null
}
},
context: Context,
@ -3677,81 +3633,11 @@ export const resolvers = {
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Расходники доступны только для фулфилмент центров')
throw new GraphQLError('Обновление цен расходников доступно только для фулфилмент центров')
}
try {
const supply = await prisma.supply.create({
data: {
name: args.input.name,
description: args.input.description,
price: args.input.price,
quantity: args.input.quantity,
unit: args.input.unit,
category: args.input.category,
status: args.input.status,
date: new Date(args.input.date),
supplier: args.input.supplier,
minStock: args.input.minStock,
currentStock: args.input.currentStock,
imageUrl: args.input.imageUrl,
organizationId: currentUser.organization.id,
},
include: { organization: true },
})
return {
success: true,
message: 'Расходник успешно создан',
supply,
}
} catch (error) {
console.error('Error creating supply:', error)
return {
success: false,
message: 'Ошибка при создании расходника',
}
}
},
// Обновить расходник
updateSupply: async (
_: unknown,
args: {
id: string
input: {
name: string
description?: string
price: number
quantity: number
unit: string
category: string
status: string
date: string
supplier: string
minStock: number
currentStock: number
imageUrl?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что расходник принадлежит текущей организации
// Находим и обновляем расходник
const existingSupply = await prisma.supply.findFirst({
where: {
id: args.id,
@ -3760,84 +3646,55 @@ export const resolvers = {
})
if (!existingSupply) {
throw new GraphQLError('Расходник не найден или нет доступа')
throw new GraphQLError('Расходник не найден')
}
try {
const supply = await prisma.supply.update({
const updatedSupply = await prisma.supply.update({
where: { id: args.id },
data: {
name: args.input.name,
description: args.input.description,
price: args.input.price,
quantity: args.input.quantity,
unit: args.input.unit,
category: args.input.category,
status: args.input.status,
date: new Date(args.input.date),
supplier: args.input.supplier,
minStock: args.input.minStock,
currentStock: args.input.currentStock,
imageUrl: args.input.imageUrl,
pricePerUnit: args.input.pricePerUnit, // Обновляем цену продажи, НЕ цену закупки
updatedAt: new Date(),
},
include: { organization: true },
})
// Преобразуем в новый формат для GraphQL
const transformedSupply = {
id: updatedSupply.id,
name: updatedSupply.name,
description: updatedSupply.description,
pricePerUnit: updatedSupply.price ? parseFloat(updatedSupply.price.toString()) : null, // Конвертируем Decimal в Number
unit: updatedSupply.unit || 'шт',
imageUrl: updatedSupply.imageUrl,
warehouseStock: updatedSupply.currentStock || 0,
isAvailable: (updatedSupply.currentStock || 0) > 0,
warehouseConsumableId: updatedSupply.id,
createdAt: updatedSupply.createdAt,
updatedAt: updatedSupply.updatedAt,
organization: updatedSupply.organization,
}
console.warn('🔥 SUPPLY PRICE UPDATED:', {
id: transformedSupply.id,
name: transformedSupply.name,
oldPrice: existingSupply.price,
newPrice: transformedSupply.pricePerUnit,
})
return {
success: true,
message: 'Расходник успешно обновлен',
supply,
message: 'Цена расходника успешно обновлена',
supply: transformedSupply,
}
} catch (error) {
console.error('Error updating supply:', error)
console.error('Error updating supply price:', error)
return {
success: false,
message: 'Ошибка при обновлении расходника',
message: 'Ошибка при обновлении цены расходника',
}
}
},
// Удалить расходник
deleteSupply: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что расходник принадлежит текущей организации
const existingSupply = await prisma.supply.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
if (!existingSupply) {
throw new GraphQLError('Расходник не найден или нет доступа')
}
try {
await prisma.supply.delete({
where: { id: args.id },
})
return true
} catch (error) {
console.error('Error deleting supply:', error)
return false
}
},
// Использовать расходники фулфилмента
useFulfillmentSupplies: async (
_: unknown,
@ -4190,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 || 'Расходники',
@ -5970,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 || 'Расходники',
@ -6730,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,

View File

@ -37,6 +37,9 @@ export const typeDefs = gql`
# Расходники селлеров (материалы клиентов)
mySupplies: [Supply!]!
# Доступные расходники для рецептур селлеров (только с ценой и в наличии)
getAvailableSuppliesForRecipe: [SupplyForRecipe!]!
# Расходники фулфилмента (материалы для работы фулфилмента)
myFulfillmentSupplies: [Supply!]!
@ -173,10 +176,8 @@ export const typeDefs = gql`
updateService(id: ID!, input: ServiceInput!): ServiceResponse!
deleteService(id: ID!): Boolean!
# Работа с расходниками
createSupply(input: SupplyInput!): SupplyResponse!
updateSupply(id: ID!, input: SupplyInput!): SupplyResponse!
deleteSupply(id: ID!): Boolean!
# Работа с расходниками (только обновление цены разрешено)
updateSupplyPrice(id: ID!, input: UpdateSupplyPriceInput!): SupplyResponse!
# Использование расходников фулфилмента
useFulfillmentSupplies(input: UseFulfillmentSuppliesInput!): SupplyResponse!
@ -278,6 +279,7 @@ export const typeDefs = gql`
ogrn: String
ogrnDate: DateTime
type: OrganizationType!
market: String
status: String
actualityDate: DateTime
registrationDate: DateTime
@ -335,6 +337,9 @@ export const typeDefs = gql`
bik: String
accountNumber: String
corrAccount: String
# Рынок для поставщиков
market: String
}
input FulfillmentRegistrationInput {
@ -516,38 +521,45 @@ export const typeDefs = gql`
id: ID!
name: String!
description: String
price: Float!
quantity: Int!
unit: String
category: String
status: String
date: DateTime!
supplier: String
minStock: Int
currentStock: Int
usedStock: Int
# Новые поля для Services архитектуры
pricePerUnit: Float # Цена за единицу для рецептур (может быть null)
unit: String! # Единица измерения: "шт", "кг", "м"
warehouseStock: Int! # Остаток на складе (readonly)
isAvailable: Boolean! # Есть ли на складе (влияет на цвет)
warehouseConsumableId: ID! # Связь со складом
# Поля из базы данных для обратной совместимости
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
type: SupplyType!
sellerOwner: Organization # Селлер-владелец (для расходников селлеров)
shopLocation: String # Местоположение в магазине фулфилмента
createdAt: DateTime!
updatedAt: DateTime!
organization: Organization!
}
input SupplyInput {
# Для рецептур селлеров - только доступные с ценой
type SupplyForRecipe {
id: ID!
name: String!
description: String
price: Float!
quantity: Int!
pricePerUnit: Float! # Всегда не null
unit: String!
category: String!
status: String!
date: DateTime!
supplier: String!
minStock: Int!
currentStock: Int!
imageUrl: String
warehouseStock: Int! # Всегда > 0
}
# Для обновления цены расходника в разделе Услуги
input UpdateSupplyPriceInput {
pricePerUnit: Float # Может быть null (цена не установлена)
}
input UseFulfillmentSuppliesInput {
@ -556,6 +568,14 @@ export const typeDefs = gql`
description: String # Описание использования (например, "Подготовка 300 продуктов")
}
# Устаревшие типы для обратной совместимости
input SupplyInput {
name: String!
description: String
price: Float!
imageUrl: String
}
type SupplyResponse {
success: Boolean!
message: String!

View File

@ -1,6 +1,5 @@
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { onError } from '@apollo/client/link/error'
// HTTP Link для GraphQL запросов
const httpLink = createHttpLink({
@ -19,87 +18,20 @@ const authLink = setContext((operation, { headers }) => {
// Приоритет у админского токена
const token = adminToken || userToken
const tokenType = adminToken ? 'admin' : 'user'
console.warn(
`Apollo Client - Operation: ${operation.operationName}, Token type: ${tokenType}, Token:`,
token ? `${token.substring(0, 20)}...` : 'No token',
)
const authHeaders = {
...headers,
authorization: token ? `Bearer ${token}` : '',
}
console.warn('Apollo Client - Auth headers:', {
authorization: authHeaders.authorization ? 'Bearer ***' : 'No auth',
})
return {
headers: authHeaders,
}
})
// Error Link для обработки ошибок с детальным логированием
const errorLink = onError(({ graphQLErrors, networkError, operation, forward: _forward }) => {
try {
// Расширенная отладочная информация для всех ошибок
const debugInfo = {
hasGraphQLErrors: !!graphQLErrors,
graphQLErrorsLength: graphQLErrors?.length || 0,
hasNetworkError: !!networkError,
operationName: operation?.operationName || 'Unknown',
operationType: (operation?.query?.definitions?.[0] as any)?.operation || 'Unknown',
variables: operation?.variables || {},
}
console.warn('🎯 APOLLO ERROR LINK TRIGGERED:', debugInfo)
// Безопасная обработка GraphQL ошибок
if (graphQLErrors && Array.isArray(graphQLErrors) && graphQLErrors.length > 0) {
console.warn('📊 GRAPHQL ERRORS COUNT:', graphQLErrors.length)
graphQLErrors.forEach((error, index) => {
try {
// Безопасная деструктуризация
const message = error?.message || 'No message'
const locations = error?.locations || []
const path = error?.path || []
const extensions = error?.extensions || {}
console.warn(`🚨 GraphQL Error #${index + 1}:`, {
message,
locations,
path,
extensions,
operation: operation?.operationName || 'Unknown',
})
} catch (innerError) {
console.warn(`❌ Error processing GraphQL error #${index + 1}:`, innerError)
}
})
}
// Безопасная обработка Network ошибок
if (networkError) {
try {
console.warn('🌐 Network Error:', {
message: networkError.message || 'No message',
statusCode: (networkError as any).statusCode || 'No status',
operation: operation?.operationName || 'Unknown',
})
} catch (innerError) {
console.warn('❌ Error processing network error:', innerError)
}
}
} catch (outerError) {
console.warn('❌ Critical error in Apollo error link:', outerError)
}
})
// Создаем Apollo Client
export const apolloClient = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
link: from([authLink, httpLink]),
cache: new InMemoryCache({
typePolicies: {
User: {

2
test-supplies.js Normal file
View File

@ -0,0 +1,2 @@
// Простой тест для проверки GraphQL запроса mySupplies
// testQuery удален из-за неиспользования