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:
@ -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,36 +595,10 @@ 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" />
|
||||
)}
|
||||
<Button variant="ghost" size="sm">
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</Button>
|
||||
</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">
|
||||
Применить
|
||||
</Button>
|
||||
</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') {
|
||||
customContacts = JSON.parse(org.managementPost)
|
||||
// Проверяем, что строка начинается с { или [, иначе это не 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,172 +112,106 @@ export function SuppliesTab() {
|
||||
const supply = editableSupplies.find((s) => s.id === supplyId)
|
||||
if (!supply) return
|
||||
|
||||
if (supply.isNew) {
|
||||
// Удаляем новую строку
|
||||
setEditableSupplies((prev) => prev.filter((s) => s.id !== supplyId))
|
||||
} else {
|
||||
// Возвращаем к исходному состоянию
|
||||
const originalSupply = supplies.find((s: Supply) => s.id === supply.id)
|
||||
if (originalSupply) {
|
||||
setEditableSupplies((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === supplyId
|
||||
? {
|
||||
id: originalSupply.id,
|
||||
name: originalSupply.name,
|
||||
description: originalSupply.description || '',
|
||||
price: originalSupply.price.toString(),
|
||||
imageUrl: originalSupply.imageUrl || '',
|
||||
isNew: false,
|
||||
isEditing: false,
|
||||
hasChanges: false,
|
||||
}
|
||||
: s,
|
||||
),
|
||||
)
|
||||
}
|
||||
// Возвращаем к исходному состоянию
|
||||
const originalSupply = supplies.find((s: Supply) => s.id === supply.id)
|
||||
if (originalSupply) {
|
||||
setEditableSupplies((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === supplyId
|
||||
? {
|
||||
id: originalSupply.id,
|
||||
name: originalSupply.name,
|
||||
description: originalSupply.description || '',
|
||||
pricePerUnit: originalSupply.pricePerUnit ? originalSupply.pricePerUnit.toString() : '',
|
||||
unit: originalSupply.unit,
|
||||
imageUrl: originalSupply.imageUrl || '',
|
||||
warehouseStock: originalSupply.warehouseStock,
|
||||
isAvailable: originalSupply.isAvailable,
|
||||
isEditing: false,
|
||||
hasChanges: false,
|
||||
}
|
||||
: s,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Обновить поле
|
||||
const updateField = (supplyId: string, field: keyof EditableSupply, value: string | File) => {
|
||||
// Обновить поле (только цену можно редактировать)
|
||||
const updateField = (supplyId: string, field: keyof EditableSupply, value: string) => {
|
||||
if (field !== 'pricePerUnit') {
|
||||
return // Только цену можно редактировать
|
||||
}
|
||||
|
||||
setEditableSupplies((prev) =>
|
||||
prev.map((supply) => {
|
||||
if (supply.id !== supplyId) return supply
|
||||
|
||||
const updated = { ...supply, hasChanges: true }
|
||||
if (field === 'imageFile' && value instanceof File) {
|
||||
updated.imageFile = value
|
||||
updated.imageUrl = URL.createObjectURL(value)
|
||||
} else if (typeof value === 'string') {
|
||||
if (field === 'name') updated.name = value
|
||||
else if (field === 'description') updated.description = value
|
||||
else if (field === 'price') updated.price = value
|
||||
else if (field === 'imageUrl') updated.imageUrl = value
|
||||
return {
|
||||
...supply,
|
||||
pricePerUnit: value,
|
||||
hasChanges: true,
|
||||
}
|
||||
return updated
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Загрузка изображения
|
||||
const uploadImageAndGetUrl = async (file: File): Promise<string> => {
|
||||
if (!user?.id) throw new Error('User not found')
|
||||
// Image upload no longer needed - supplies are readonly except price
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('userId', user.id)
|
||||
formData.append('type', 'supply')
|
||||
|
||||
const response = await fetch('/api/upload-service-image', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to upload image')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.url
|
||||
}
|
||||
|
||||
// Сохранить все изменения
|
||||
// Сохранить все изменения (только цены)
|
||||
const saveAllChanges = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const suppliesToSave = editableSupplies.filter((s) => {
|
||||
if (s.isNew) {
|
||||
// Для новых записей проверяем что обязательные поля заполнены
|
||||
return s.name.trim() && s.price
|
||||
}
|
||||
// Для существующих записей проверяем флаг изменений
|
||||
return s.hasChanges
|
||||
})
|
||||
|
||||
console.warn('Supplies to save:', suppliesToSave.length, suppliesToSave)
|
||||
const suppliesToSave = editableSupplies.filter((s) => s.hasChanges)
|
||||
|
||||
for (const supply of suppliesToSave) {
|
||||
if (!supply.name.trim() || !supply.price) {
|
||||
toast.error('Заполните обязательные поля для всех расходников')
|
||||
// Проверяем валидность цены (может быть пустой)
|
||||
const pricePerUnit = supply.pricePerUnit.trim() ? parseFloat(supply.pricePerUnit) : null
|
||||
|
||||
if (supply.pricePerUnit.trim() && (isNaN(pricePerUnit!) || pricePerUnit! <= 0)) {
|
||||
toast.error('Введите корректную цену')
|
||||
setIsSaving(false)
|
||||
return
|
||||
}
|
||||
|
||||
let imageUrl = supply.imageUrl
|
||||
|
||||
// Загружаем изображение если выбрано
|
||||
if (supply.imageFile) {
|
||||
imageUrl = await uploadImageAndGetUrl(supply.imageFile)
|
||||
}
|
||||
|
||||
const input = {
|
||||
name: supply.name,
|
||||
description: supply.description || undefined,
|
||||
price: parseFloat(supply.price),
|
||||
imageUrl: imageUrl || undefined,
|
||||
pricePerUnit: pricePerUnit,
|
||||
}
|
||||
|
||||
if (supply.isNew) {
|
||||
await createSupply({
|
||||
variables: { input },
|
||||
update: (cache, { data }) => {
|
||||
if (data?.createSupply?.supply) {
|
||||
const existingData = cache.readQuery({ query: GET_MY_SUPPLIES }) as { mySupplies: Supply[] } | null
|
||||
if (existingData) {
|
||||
cache.writeQuery({
|
||||
query: GET_MY_SUPPLIES,
|
||||
data: {
|
||||
mySupplies: [...existingData.mySupplies, data.createSupply.supply],
|
||||
},
|
||||
})
|
||||
}
|
||||
await updateSupplyPrice({
|
||||
variables: { id: supply.id, input },
|
||||
update: (cache, { data }) => {
|
||||
if (data?.updateSupplyPrice?.supply) {
|
||||
const existingData = cache.readQuery({ query: GET_MY_SUPPLIES }) as { mySupplies: Supply[] } | null
|
||||
if (existingData) {
|
||||
cache.writeQuery({
|
||||
query: GET_MY_SUPPLIES,
|
||||
data: {
|
||||
mySupplies: existingData.mySupplies.map((s: Supply) =>
|
||||
s.id === data.updateSupplyPrice.supply.id ? data.updateSupplyPrice.supply : s,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
} else if (supply.id) {
|
||||
await updateSupply({
|
||||
variables: { id: supply.id, input },
|
||||
update: (cache, { data }) => {
|
||||
if (data?.updateSupply?.supply) {
|
||||
const existingData = cache.readQuery({ query: GET_MY_SUPPLIES }) as { mySupplies: Supply[] } | null
|
||||
if (existingData) {
|
||||
cache.writeQuery({
|
||||
query: GET_MY_SUPPLIES,
|
||||
data: {
|
||||
mySupplies: existingData.mySupplies.map((s: Supply) =>
|
||||
s.id === data.updateSupply.supply.id ? data.updateSupply.supply : s,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Удаления теперь происходят сразу в removeRow, так что здесь обрабатываем только обновления
|
||||
// Сбрасываем флаги изменений
|
||||
setEditableSupplies((prev) => prev.map((s) => ({ ...s, hasChanges: false, isEditing: false })))
|
||||
|
||||
toast.success('Все изменения успешно сохранены')
|
||||
setPendingChanges([])
|
||||
toast.success('Цены успешно обновлены')
|
||||
} catch (error) {
|
||||
console.error('Error saving changes:', error)
|
||||
toast.error('Ошибка при сохранении изменений')
|
||||
toast.error('Ошибка при сохранении цен')
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем есть ли несохраненные изменения
|
||||
// Проверяем есть ли несохраненные изменения (только цены)
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
return editableSupplies.some((s) => {
|
||||
if (s.isNew) {
|
||||
// Для новых записей проверяем что есть данные для сохранения
|
||||
return s.name.trim() || s.price || s.description.trim()
|
||||
}
|
||||
return s.hasChanges
|
||||
})
|
||||
return editableSupplies.some((s) => s.hasChanges)
|
||||
}, [editableSupplies])
|
||||
|
||||
return (
|
||||
@ -323,19 +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>
|
||||
)}
|
||||
<span className="text-white font-medium">{supply.name}</span>
|
||||
</td>
|
||||
|
||||
{/* Цена */}
|
||||
{/* Остаток на складе */}
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`text-sm font-medium ${supply.isAvailable ? 'text-green-400' : 'text-red-400'}`}
|
||||
>
|
||||
{supply.warehouseStock}
|
||||
</span>
|
||||
{!supply.isAvailable && (
|
||||
<span className="px-2 py-1 rounded-full bg-red-500/20 text-red-300 text-xs">
|
||||
Нет в наличии
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Единица измерения */}
|
||||
<td className="p-4">
|
||||
<span className="text-white/80">{supply.unit}</span>
|
||||
</td>
|
||||
|
||||
{/* Цена за единицу */}
|
||||
<td className="p-4">
|
||||
{supply.isEditing ? (
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={supply.price}
|
||||
onChange={(e) => updateField(supply.id!, 'price', e.target.value)}
|
||||
value={supply.pricePerUnit}
|
||||
onChange={(e) => updateField(supply.id, 'pricePerUnit', e.target.value)}
|
||||
className="bg-white/5 border-white/20 text-white"
|
||||
placeholder="0.00"
|
||||
placeholder="Не установлена"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80">
|
||||
{supply.price ? parseFloat(supply.price).toLocaleString() : '0'} ₽
|
||||
{supply.pricePerUnit
|
||||
? `${parseFloat(supply.pricePerUnit).toLocaleString()} ₽`
|
||||
: 'Не установлена'}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Описание */}
|
||||
<td className="p-4">
|
||||
{supply.isEditing ? (
|
||||
<Input
|
||||
value={supply.description}
|
||||
onChange={(e) => updateField(supply.id!, 'description', e.target.value)}
|
||||
className="bg-white/5 border-white/20 text-white"
|
||||
placeholder="Описание расходника"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80">{supply.description || '—'}</span>
|
||||
)}
|
||||
<span className="text-white/80">{supply.description || '—'}</span>
|
||||
</td>
|
||||
|
||||
{/* Действия */}
|
||||
@ -556,14 +423,9 @@ export function SuppliesTab() {
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Сохраняем только этот расходник если заполнены обязательные поля
|
||||
if (supply.name.trim() && supply.price) {
|
||||
saveAllChanges()
|
||||
} else {
|
||||
toast.error('Заполните обязательные поля')
|
||||
}
|
||||
saveAllChanges()
|
||||
}}
|
||||
disabled={!supply.name.trim() || !supply.price || isSaving}
|
||||
disabled={isSaving}
|
||||
className="h-8 w-8 p-0 bg-gradient-to-r from-green-500/20 to-emerald-500/20 hover:from-green-500/30 hover:to-emerald-500/30 border border-green-500/30 hover:border-green-400/50 text-green-300 hover:text-white transition-all duration-200 shadow-lg shadow-green-500/10 hover:shadow-green-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Сохранить"
|
||||
>
|
||||
@ -571,7 +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,40 +694,37 @@ 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" />
|
||||
</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>
|
||||
<Building2 className="h-5 w-5 text-blue-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Поставщики</h2>
|
||||
</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>
|
||||
{allCounterparties.length === 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push('/market')}
|
||||
className="glass-secondary hover:text-white/90 transition-all duration-200 mt-2 w-full"
|
||||
>
|
||||
<Building2 className="h-3 w-3 mr-2" />
|
||||
Найти поставщиков в маркете
|
||||
</Button>
|
||||
)}
|
||||
</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"
|
||||
>
|
||||
<Building2 className="h-3 w-3 mr-2" />
|
||||
Найти поставщиков в маркете
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Список поставщиков согласно visual-design-rules.md */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{isLoading ? (
|
||||
@ -790,32 +777,17 @@ 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>
|
||||
<h4 className="text-white font-medium text-sm truncate group-hover:text-white transition-colors">
|
||||
{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={`text-xs font-medium border ${getMarketBadgeStyle(supplier.market)}`}>
|
||||
{getMarketLabel(supplier.market)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-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}
|
||||
</Badge>
|
||||
</div>
|
||||
{supplier.address && (
|
||||
<p className="text-white/50 text-xs line-clamp-1">{supplier.address}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -826,158 +798,118 @@ 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>
|
||||
{/* БЛОК 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 w-20 h-28 border-white/10 hover:border-white/30"
|
||||
onClick={() => {
|
||||
// Добавляем товар в детальный каталог (блок 3)
|
||||
if (!allSelectedProducts.find((p) => p.id === product.id)) {
|
||||
setAllSelectedProducts((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...product,
|
||||
selectedQuantity: 1,
|
||||
supplierId: selectedSupplier.id,
|
||||
supplierName: selectedSupplier.name || selectedSupplier.fullName || 'Поставщик',
|
||||
},
|
||||
])
|
||||
}
|
||||
}}
|
||||
>
|
||||
{product.mainImage ? (
|
||||
<Image
|
||||
src={product.mainImage}
|
||||
alt={product.name}
|
||||
width={80}
|
||||
height={112}
|
||||
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 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
|
||||
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}
|
||||
onClick={() => {
|
||||
// Добавляем товар в детальный каталог (блок 3)
|
||||
if (!allSelectedProducts.find((p) => p.id === product.id)) {
|
||||
setAllSelectedProducts((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...product,
|
||||
selectedQuantity: 1,
|
||||
supplierId: selectedSupplier.id,
|
||||
supplierName: selectedSupplier.name || selectedSupplier.fullName || 'Поставщик',
|
||||
},
|
||||
])
|
||||
}
|
||||
}}
|
||||
>
|
||||
{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"
|
||||
/>
|
||||
) : (
|
||||
<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">
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Детальный каталог ({allSelectedProducts.length} товаров)
|
||||
</h3>
|
||||
<p className="text-white/60 text-sm mt-1">Товары из блока карточек для детального управления</p>
|
||||
</div>
|
||||
{/* ВРЕМЕННО ОТКЛЮЧЕНО: Верхняя панель согласно правилам 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" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Детальный каталог ({allSelectedProducts.length} товаров)
|
||||
</h3>
|
||||
<p className="text-white/60 text-sm mt-1">Товары для детального управления поставкой</p>
|
||||
</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>
|
||||
|
||||
@ -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,13 +83,16 @@ 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)
|
||||
@ -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,65 +623,33 @@ 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
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_SUPPLY = gql`
|
||||
mutation UpdateSupply($id: ID!, $input: SupplyInput!) {
|
||||
updateSupply(id: $id, input: $input) {
|
||||
success
|
||||
message
|
||||
supply {
|
||||
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)
|
||||
}
|
||||
`
|
||||
|
||||
// Мутация для заказа поставки расходников
|
||||
export const CREATE_SUPPLY_ORDER = gql`
|
||||
mutation CreateSupplyOrder($input: SupplyOrderInput!) {
|
||||
@ -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,167 +3633,68 @@ 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,
|
||||
// Находим и обновляем расходник
|
||||
const existingSupply = await prisma.supply.findFirst({
|
||||
where: {
|
||||
id: args.id,
|
||||
organizationId: currentUser.organization.id,
|
||||
},
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Расходник успешно создан',
|
||||
supply,
|
||||
if (!existingSupply) {
|
||||
throw new GraphQLError('Расходник не найден')
|
||||
}
|
||||
} 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,
|
||||
organizationId: currentUser.organization.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!existingSupply) {
|
||||
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: {
|
||||
|
Reference in New Issue
Block a user