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:
1988
dev.log
Normal file
1988
dev.log
Normal 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
|
@ -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",
|
||||
|
@ -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("Расходники")
|
||||
|
1437
rules-complete.md
1437
rules-complete.md
@ -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) 🚚 ЛОГИСТИКА**:
|
||||
- **Создание маршрутов доставки** (откуда → куда)
|
||||
- **Тарификация**: цена до 1м³ и свыше 1м³
|
||||
|
||||
- **Полный 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**: Указание источников и осторожные формулировки
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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}</>
|
||||
}
|
||||
|
@ -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]: {
|
||||
|
@ -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
|
||||
|
@ -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' : ''
|
||||
}`}
|
||||
|
@ -12,10 +12,8 @@ import {
|
||||
Phone,
|
||||
Mail,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
Calendar,
|
||||
MessageCircle,
|
||||
User,
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
|
@ -21,7 +21,6 @@ import {
|
||||
Truck,
|
||||
Warehouse,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 }) {
|
||||
|
@ -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,
|
||||
|
@ -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'
|
||||
|
||||
// Типы данных для товаров ФФ
|
||||
|
@ -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'
|
||||
|
@ -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">
|
||||
Вы действительно хотите удалить расходник “{supply.name}”? Это действие
|
||||
необратимо.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="gap-3">
|
||||
<AlertDialogCancel className="border-red-400/30 text-red-200 hover:bg-red-500/10 hover:border-red-300 transition-all duration-300">
|
||||
Отмена
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => removeRow(supply.id!, supply.isNew)}
|
||||
className="bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 text-white border-0 shadow-lg shadow-red-500/25 hover:shadow-red-500/40 transition-all duration-300"
|
||||
>
|
||||
Удалить
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -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">
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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">
|
||||
|
@ -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) => {
|
||||
|
@ -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"
|
||||
/>
|
||||
|
||||
{/* Количество в наличии */}
|
||||
|
@ -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">
|
||||
|
@ -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)}
|
||||
/>
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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 || '',
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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!
|
||||
|
@ -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
2
test-supplies.js
Normal file
@ -0,0 +1,2 @@
|
||||
// Простой тест для проверки GraphQL запроса mySupplies
|
||||
// testQuery удален из-за неиспользования
|
Reference in New Issue
Block a user