feat(supplier-orders): добавить параметры поставки в таблицу заявок
- Добавлены колонки Объём и Грузовые места между Цена товаров и Статус - Реализованы инпуты для ввода volume и packagesCount в статусе PENDING для роли WHOLESALE - Добавлена мутация UPDATE_SUPPLY_PARAMETERS с проверками безопасности - Скрыта строка Поставщик для роли WHOLESALE (поставщик знает свои данные) - Исправлено выравнивание таблицы при скрытии уровня поставщика - Реорганизованы документы: legacy-rules/, docs/, docs-and-reports/ ВНИМАНИЕ: Компонент multilevel-supplies-table.tsx (1697 строк) нарушает правило модульной архитектуры (>800 строк требует рефакторинга) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,18 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Clock, CheckCircle, Settings, Truck, Package, Calendar, Search } from 'lucide-react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Clock, CheckCircle, Settings, Truck, Package } from 'lucide-react'
|
||||
import { useState, useMemo, useCallback, useRef } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { MultiLevelSuppliesTable } from '@/components/supplies/multilevel-supplies-table'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { SUPPLIER_APPROVE_ORDER, SUPPLIER_REJECT_ORDER, SUPPLIER_SHIP_ORDER } from '@/graphql/mutations'
|
||||
import { GET_SUPPLY_ORDERS, GET_MY_SUPPLY_ORDERS } from '@/graphql/queries'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { SUPPLIER_APPROVE_ORDER, SUPPLIER_REJECT_ORDER, SUPPLIER_SHIP_ORDER, UPDATE_SUPPLY_PARAMETERS } from '@/graphql/mutations'
|
||||
import { GET_MY_SUPPLY_ORDERS } from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
|
||||
@ -130,7 +127,7 @@ export function SupplierOrdersTabs() {
|
||||
})
|
||||
|
||||
// Мутации для действий поставщика
|
||||
const [supplierApproveOrder, { loading: approving }] = useMutation(SUPPLIER_APPROVE_ORDER, {
|
||||
const [supplierApproveOrder] = useMutation(SUPPLIER_APPROVE_ORDER, {
|
||||
refetchQueries: [{ query: GET_MY_SUPPLY_ORDERS }],
|
||||
onCompleted: (data) => {
|
||||
if (data.supplierApproveOrder.success) {
|
||||
@ -145,7 +142,7 @@ export function SupplierOrdersTabs() {
|
||||
},
|
||||
})
|
||||
|
||||
const [supplierRejectOrder, { loading: rejecting }] = useMutation(SUPPLIER_REJECT_ORDER, {
|
||||
const [supplierRejectOrder] = useMutation(SUPPLIER_REJECT_ORDER, {
|
||||
refetchQueries: [{ query: GET_MY_SUPPLY_ORDERS }],
|
||||
onCompleted: (data) => {
|
||||
if (data.supplierRejectOrder.success) {
|
||||
@ -160,7 +157,7 @@ export function SupplierOrdersTabs() {
|
||||
},
|
||||
})
|
||||
|
||||
const [supplierShipOrder, { loading: shipping }] = useMutation(SUPPLIER_SHIP_ORDER, {
|
||||
const [supplierShipOrder] = useMutation(SUPPLIER_SHIP_ORDER, {
|
||||
refetchQueries: [{ query: GET_MY_SUPPLY_ORDERS }],
|
||||
onCompleted: (data) => {
|
||||
if (data.supplierShipOrder.success) {
|
||||
@ -175,6 +172,91 @@ export function SupplierOrdersTabs() {
|
||||
},
|
||||
})
|
||||
|
||||
// Мутация для обновления параметров поставки (объём и грузовые места)
|
||||
const [updateSupplyParameters] = useMutation(UPDATE_SUPPLY_PARAMETERS, {
|
||||
update: (cache, { data }) => {
|
||||
if (data?.updateSupplyParameters.success) {
|
||||
// Обновляем кеш Apollo напрямую
|
||||
const existingData = cache.readQuery({ query: GET_MY_SUPPLY_ORDERS })
|
||||
if (existingData?.mySupplyOrders) {
|
||||
const updatedOrders = existingData.mySupplyOrders.map((order: any) =>
|
||||
order.id === data.updateSupplyParameters.order.id
|
||||
? { ...order, ...data.updateSupplyParameters.order }
|
||||
: order,
|
||||
)
|
||||
cache.writeQuery({
|
||||
query: GET_MY_SUPPLY_ORDERS,
|
||||
data: { mySupplyOrders: updatedOrders },
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data.updateSupplyParameters.success) {
|
||||
// Parameters updated successfully
|
||||
// Сбрасываем pending состояние для обновленных полей
|
||||
const updatedOrder = data.updateSupplyParameters.order
|
||||
if ((window as any).__handleUpdateComplete) {
|
||||
if (updatedOrder.volume !== null) {
|
||||
(window as any).__handleUpdateComplete(updatedOrder.id, 'volume')
|
||||
}
|
||||
if (updatedOrder.packagesCount !== null) {
|
||||
(window as any).__handleUpdateComplete(updatedOrder.id, 'packages')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast.error(data.updateSupplyParameters.message)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error updating supply parameters:', error)
|
||||
toast.error('Ошибка при обновлении параметров поставки')
|
||||
},
|
||||
})
|
||||
|
||||
// Debounced обработчики для инпутов с задержкой
|
||||
const debounceTimeouts = useRef<{ [key: string]: NodeJS.Timeout }>({})
|
||||
|
||||
const handleVolumeChange = useCallback((supplyId: string, volume: number | null) => {
|
||||
// Handle volume change with debounce
|
||||
|
||||
// Очистить предыдущий таймер для данной поставки
|
||||
if (debounceTimeouts.current[`volume-${supplyId}`]) {
|
||||
clearTimeout(debounceTimeouts.current[`volume-${supplyId}`])
|
||||
}
|
||||
|
||||
// Установить новый таймер с задержкой 500ms
|
||||
debounceTimeouts.current[`volume-${supplyId}`] = setTimeout(() => {
|
||||
// Sending volume update
|
||||
updateSupplyParameters({
|
||||
variables: {
|
||||
id: supplyId,
|
||||
volume: volume,
|
||||
},
|
||||
})
|
||||
}, 500)
|
||||
}, [updateSupplyParameters])
|
||||
|
||||
const handlePackagesChange = useCallback((supplyId: string, packagesCount: number | null) => {
|
||||
// Handle packages change with debounce
|
||||
|
||||
// Очистить предыдущий таймер для данной поставки
|
||||
if (debounceTimeouts.current[`packages-${supplyId}`]) {
|
||||
clearTimeout(debounceTimeouts.current[`packages-${supplyId}`])
|
||||
}
|
||||
|
||||
// Установить новый таймер с задержкой 500ms
|
||||
debounceTimeouts.current[`packages-${supplyId}`] = setTimeout(() => {
|
||||
// Sending packages update
|
||||
updateSupplyParameters({
|
||||
variables: {
|
||||
id: supplyId,
|
||||
packagesCount: packagesCount,
|
||||
},
|
||||
})
|
||||
}, 500)
|
||||
}, [updateSupplyParameters])
|
||||
|
||||
// Получаем заказы поставок с многоуровневой структурой
|
||||
const supplierOrders: SupplyOrder[] = useMemo(() => {
|
||||
return data?.mySupplyOrders || []
|
||||
@ -247,11 +329,11 @@ export function SupplierOrdersTabs() {
|
||||
await supplierShipOrder({ variables: { id: supplyId } })
|
||||
break
|
||||
case 'cancel':
|
||||
console.log('Отмена поставки:', supplyId)
|
||||
// Cancel supply order
|
||||
// TODO: Реализовать отмену поставки если нужно
|
||||
break
|
||||
default:
|
||||
console.log('Неизвестное действие:', action, supplyId)
|
||||
console.error('Неизвестное действие:', action, supplyId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выполнении действия:', error)
|
||||
@ -389,7 +471,10 @@ export function SupplierOrdersTabs() {
|
||||
<MultiLevelSuppliesTable
|
||||
supplies={getCurrentOrders()}
|
||||
userRole="WHOLESALE"
|
||||
activeTab={activeTab}
|
||||
onSupplyAction={handleSupplierAction}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
onPackagesChange={handlePackagesChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Package, Building2, MapPin, Truck, Clock, Calendar, Settings } from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@ -121,7 +121,11 @@ interface MultiLevelSuppliesTableProps {
|
||||
supplies?: SupplyOrderFromGraphQL[]
|
||||
loading?: boolean
|
||||
userRole?: 'SELLER' | 'WHOLESALE' | 'FULFILLMENT' | 'LOGIST'
|
||||
activeTab?: string
|
||||
onSupplyAction?: (supplyId: string, action: string) => void
|
||||
onVolumeChange?: (supplyId: string, volume: number | null) => void
|
||||
onPackagesChange?: (supplyId: string, packagesCount: number | null) => void
|
||||
onUpdateComplete?: (supplyId: string, field: 'volume' | 'packages') => void
|
||||
}
|
||||
|
||||
// Простые компоненты таблицы
|
||||
@ -177,6 +181,42 @@ const TableCell = ({
|
||||
</td>
|
||||
)
|
||||
|
||||
// ActionButtons компонент для кнопок действий поставщика
|
||||
function ActionButtons({
|
||||
supplyId,
|
||||
onSupplyAction
|
||||
}: {
|
||||
supplyId: string
|
||||
onSupplyAction?: (supplyId: string, action: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="glass-button bg-gradient-to-r from-green-500 to-emerald-500 text-white text-xs font-medium h-8 px-3 hover:scale-105 transition-all duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onSupplyAction?.(supplyId, 'approve')
|
||||
}}
|
||||
>
|
||||
Одобрить
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-red-600 to-red-500 hover:from-red-700 hover:to-red-600 text-white text-xs font-medium h-8 px-3 hover:scale-105 transition-all duration-200 backdrop-blur border border-red-500/30"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onSupplyAction?.(supplyId, 'reject')
|
||||
}}
|
||||
>
|
||||
Отклонить
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Компонент для статуса поставки
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const getStatusColor = (status: string) => {
|
||||
@ -316,12 +356,68 @@ export function MultiLevelSuppliesTable({
|
||||
supplies = [],
|
||||
loading: _loading = false,
|
||||
userRole = 'SELLER',
|
||||
activeTab,
|
||||
onSupplyAction,
|
||||
onVolumeChange,
|
||||
onPackagesChange,
|
||||
onUpdateComplete,
|
||||
}: MultiLevelSuppliesTableProps) {
|
||||
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(new Set())
|
||||
const [expandedRoutes, setExpandedRoutes] = useState<Set<string>>(new Set())
|
||||
const [expandedSuppliers, setExpandedSuppliers] = useState<Set<string>>(new Set())
|
||||
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set())
|
||||
|
||||
// Локальное состояние для инпутов
|
||||
const [inputValues, setInputValues] = useState<{[key: string]: {volume: string, packages: string}}>({})
|
||||
// Отслеживание, какие инпуты редактируются (пока не придет ответ от сервера)
|
||||
const [pendingUpdates, setPendingUpdates] = useState<Set<string>>(new Set())
|
||||
|
||||
// Синхронизация локального состояния с данными поставок
|
||||
useEffect(() => {
|
||||
setInputValues(prev => {
|
||||
const newValues: {[key: string]: {volume: string, packages: string}} = {}
|
||||
supplies.forEach(supply => {
|
||||
// Не перезаписываем значения для инпутов с ожидающими обновлениями
|
||||
const isVolumePending = pendingUpdates.has(`${supply.id}-volume`)
|
||||
const isPackagesPending = pendingUpdates.has(`${supply.id}-packages`)
|
||||
|
||||
newValues[supply.id] = {
|
||||
volume: isVolumePending ? (prev[supply.id]?.volume ?? '') : (supply.volume?.toString() ?? ''),
|
||||
packages: isPackagesPending ? (prev[supply.id]?.packages ?? '') : (supply.packagesCount?.toString() ?? '')
|
||||
}
|
||||
})
|
||||
|
||||
// Проверяем, нужно ли обновление
|
||||
const hasChanges = supplies.some(supply => {
|
||||
const isVolumePending = pendingUpdates.has(`${supply.id}-volume`)
|
||||
const isPackagesPending = pendingUpdates.has(`${supply.id}-packages`)
|
||||
|
||||
if (isVolumePending || isPackagesPending) return false
|
||||
|
||||
const volumeChanged = prev[supply.id]?.volume !== (supply.volume?.toString() ?? '')
|
||||
const packagesChanged = prev[supply.id]?.packages !== (supply.packagesCount?.toString() ?? '')
|
||||
|
||||
return volumeChanged || packagesChanged
|
||||
})
|
||||
|
||||
return hasChanges ? newValues : prev
|
||||
})
|
||||
}, [supplies, pendingUpdates])
|
||||
|
||||
// Обработчик завершения обновления для сброса pending состояния
|
||||
useEffect(() => {
|
||||
if (onUpdateComplete) {
|
||||
const handleUpdateComplete = (supplyId: string, field: 'volume' | 'packages') => {
|
||||
setPendingUpdates(prev => {
|
||||
const newSet = new Set(prev)
|
||||
newSet.delete(`${supplyId}-${field}`)
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
// Эта функция будет вызываться из родительского компонента
|
||||
(window as any).__handleUpdateComplete = handleUpdateComplete
|
||||
}
|
||||
}, [onUpdateComplete])
|
||||
|
||||
// Состояния для контекстного меню
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
@ -335,25 +431,6 @@ export function MultiLevelSuppliesTable({
|
||||
})
|
||||
const [cancelDialogOpen, setCancelDialogOpen] = useState(false)
|
||||
|
||||
// Диагностика данных услуг ФФ (только в dev режиме)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(
|
||||
'🔍 ДИАГНОСТИКА: Данные поставок и рецептур:',
|
||||
supplies.map((supply) => ({
|
||||
id: supply.id,
|
||||
itemsCount: supply.items?.length || 0,
|
||||
items: supply.items?.slice(0, 2).map((item) => ({
|
||||
id: item.id,
|
||||
productName: item.product?.name,
|
||||
hasRecipe: !!item.recipe,
|
||||
recipe: item.recipe,
|
||||
services: item.services,
|
||||
fulfillmentConsumables: item.fulfillmentConsumables,
|
||||
sellerConsumables: item.sellerConsumables,
|
||||
})),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
// Массив цветов для различения поставок (с лучшим контрастом)
|
||||
const supplyColors = [
|
||||
@ -569,8 +646,10 @@ export function MultiLevelSuppliesTable({
|
||||
const routes = supply.routes || []
|
||||
|
||||
const orderedTotal = items.reduce((sum, item) => sum + (item.quantity || 0), 0)
|
||||
/* ОТКАТ: Удаление колонок "Поставлено" и "Брак" */
|
||||
const deliveredTotal = 0 // Пока нет данных о поставленном количестве
|
||||
const defectTotal = 0 // Пока нет данных о браке
|
||||
/* /ОТКАТ */
|
||||
|
||||
const goodsPrice = items.reduce((sum, item) => sum + (item.totalPrice || 0), 0)
|
||||
|
||||
@ -617,8 +696,10 @@ export function MultiLevelSuppliesTable({
|
||||
|
||||
return {
|
||||
orderedTotal,
|
||||
/* ОТКАТ: Удаление колонок "Поставлено" и "Брак" */
|
||||
deliveredTotal,
|
||||
defectTotal,
|
||||
/* /ОТКАТ */
|
||||
goodsPrice,
|
||||
servicesPrice,
|
||||
ffConsumablesPrice,
|
||||
@ -648,9 +729,17 @@ export function MultiLevelSuppliesTable({
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">№</TableHead>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Дата поставки</TableHead>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Заказано</TableHead>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Поставлено</TableHead>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Брак</TableHead>
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE не видит колонки "Поставлено" и "Брак" */}
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Поставлено</TableHead>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Брак</TableHead>
|
||||
</>
|
||||
)}
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Цена товаров</TableHead>
|
||||
{/* 🆕 НОВЫЕ КОЛОНКИ: Объём и грузовые места между "Цена товаров" и "Статус" */}
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Объём</TableHead>
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Грузовые места</TableHead>
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE не видит услуги ФФ, расходники и логистику */}
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap hidden lg:table-cell">
|
||||
@ -672,9 +761,11 @@ export function MultiLevelSuppliesTable({
|
||||
Логистика до ФФ
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">
|
||||
{userRole === 'WHOLESALE' ? 'Мои товары' : 'Итого'}
|
||||
</TableHead>
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">
|
||||
Итого
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="text-white/90 text-sm font-light whitespace-nowrap">Статус</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@ -697,10 +788,6 @@ export function MultiLevelSuppliesTable({
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors"
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
backgroundColor: getLevelBackgroundColor(1, index),
|
||||
}}
|
||||
onClick={() => {
|
||||
@ -740,23 +827,90 @@ export function MultiLevelSuppliesTable({
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">{aggregatedData.orderedTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">{aggregatedData.deliveredTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`font-semibold text-sm ${
|
||||
(aggregatedData.defectTotal || 0) > 0 ? 'text-red-400' : 'text-white'
|
||||
}`}
|
||||
>
|
||||
{aggregatedData.defectTotal}
|
||||
</span>
|
||||
</TableCell>
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE не видит колонки "Поставлено" и "Брак" */}
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.deliveredTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.defectTotal}</span>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
<TableCell>
|
||||
<span className="text-green-400 font-semibold text-sm">
|
||||
{formatCurrency(aggregatedData.goodsPrice)}
|
||||
</span>
|
||||
</TableCell>
|
||||
{/* 🆕 НОВЫЕ ЯЧЕЙКИ: Объём и грузовые места с инпутами для WHOLESALE */}
|
||||
<TableCell>
|
||||
{userRole === 'WHOLESALE' && supply.status === 'PENDING' ? (
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.0 м³"
|
||||
value={inputValues[supply.id]?.volume ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
// Устанавливаем pending состояние
|
||||
setPendingUpdates(prev => new Set(prev).add(`${supply.id}-volume`))
|
||||
// Обновляем локальное состояние немедленно
|
||||
setInputValues(prev => ({
|
||||
...prev,
|
||||
[supply.id]: {
|
||||
...prev[supply.id],
|
||||
volume: value
|
||||
}
|
||||
}))
|
||||
// Вызываем обработчик с преобразованным значением
|
||||
const numValue = value === '' ? null : parseFloat(value)
|
||||
onVolumeChange?.(supply.id, numValue)
|
||||
}}
|
||||
onFocus={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="w-20 bg-white/10 border border-white/20 rounded px-2 py-1 text-white text-sm placeholder:text-white/40 focus:outline-none focus:border-white/40"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80 text-sm">
|
||||
{supply.volume ? `${supply.volume} м³` : '—'}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{userRole === 'WHOLESALE' && supply.status === 'PENDING' ? (
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0 мест"
|
||||
value={inputValues[supply.id]?.packages ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
// Устанавливаем pending состояние
|
||||
setPendingUpdates(prev => new Set(prev).add(`${supply.id}-packages`))
|
||||
// Обновляем локальное состояние немедленно
|
||||
setInputValues(prev => ({
|
||||
...prev,
|
||||
[supply.id]: {
|
||||
...prev[supply.id],
|
||||
packages: value
|
||||
}
|
||||
}))
|
||||
// Вызываем обработчик с преобразованным значением
|
||||
const numValue = value === '' ? null : parseInt(value)
|
||||
onPackagesChange?.(supply.id, numValue)
|
||||
}}
|
||||
onFocus={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="w-20 bg-white/10 border border-white/20 rounded px-2 py-1 text-white text-sm placeholder:text-white/40 focus:outline-none focus:border-white/40"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/80 text-sm">
|
||||
{supply.packagesCount ? `${supply.packagesCount} мест` : '—'}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE не видит услуги ФФ, расходники и логистику */}
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
@ -786,28 +940,31 @@ export function MultiLevelSuppliesTable({
|
||||
</span>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE видит только стоимость своих товаров */}
|
||||
<span className="text-white font-bold text-sm">
|
||||
{formatCurrency(userRole === 'WHOLESALE' ? aggregatedData.goodsPrice : aggregatedData.total)}
|
||||
</span>
|
||||
|
||||
{/* ОТКАТ: Со значком доллара
|
||||
<div className="flex items-center space-x-1">
|
||||
<DollarSign className="h-3 w-3 text-white/40" />
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<TableCell>
|
||||
<span className="text-white font-bold text-sm">
|
||||
{formatCurrency(aggregatedData.total)}
|
||||
</span>
|
||||
</div>
|
||||
*/}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>
|
||||
{/* 🔒 УСЛОВНЫЙ РЕНДЕРИНГ: Кнопки для WHOLESALE во вкладке "Новые" */}
|
||||
{userRole === 'WHOLESALE' && activeTab === 'new' && supply.status === 'PENDING' ? (
|
||||
<ActionButtons
|
||||
supplyId={supply.id}
|
||||
onSupplyAction={onSupplyAction}
|
||||
/>
|
||||
) : (
|
||||
<StatusBadge status={supply.status} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{userRole !== 'WHOLESALE' && <StatusBadge status={supply.status} />}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* ВАРИАНТ 1: Строка с ID поставки между уровнями */}
|
||||
{isSupplyExpanded && (
|
||||
<TableRow className="border-0 bg-white/5">
|
||||
<TableCell colSpan={userRole === 'WHOLESALE' ? 8 : 12} className="py-2 px-4 relative">
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: colSpan учитывает скрытые колонки для WHOLESALE + новые колонки объём/грузовые места */}
|
||||
<TableCell colSpan={userRole === 'WHOLESALE' ? 7 : 9} className="py-2 px-4 relative">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-white/60 text-xs">ID поставки:</span>
|
||||
<span className="text-white/80 text-xs font-mono">{supply.id.slice(-8).toUpperCase()}</span>
|
||||
@ -824,8 +981,8 @@ export function MultiLevelSuppliesTable({
|
||||
{/* Строка с ID убрана */}
|
||||
{/* */}
|
||||
|
||||
{/* УРОВЕНЬ 2: Маршруты поставки */}
|
||||
{isSupplyExpanded &&
|
||||
{/* УРОВЕНЬ 2: Маршруты поставки - скрыто для WHOLESALE */}
|
||||
{isSupplyExpanded && userRole !== 'WHOLESALE' &&
|
||||
(() => {
|
||||
// ✅ ВРЕМЕННАЯ ЗАГЛУШКА: создаем фиктивный маршрут для демонстрации
|
||||
const mockRoutes =
|
||||
@ -880,12 +1037,17 @@ export function MultiLevelSuppliesTable({
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.orderedTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.deliveredTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.defectTotal}</span>
|
||||
</TableCell>
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE не видит колонки "Поставлено" и "Брак" на route level */}
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.deliveredTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.defectTotal}</span>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
<TableCell>
|
||||
<span className="text-green-400 font-medium text-sm">
|
||||
{formatCurrency(aggregatedData.goodsPrice)}
|
||||
@ -974,25 +1136,24 @@ export function MultiLevelSuppliesTable({
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.orderedTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.deliveredTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.defectTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-green-400 font-medium text-sm">
|
||||
{formatCurrency(aggregatedData.goodsPrice)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell colSpan={userRole === 'WHOLESALE' ? 0 : 4} className="text-right pr-8">
|
||||
{/* Агрегированные данные поставщика отображаются только в итого */}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{formatCurrency(aggregatedData.total)}
|
||||
</span>
|
||||
</TableCell>
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<>
|
||||
{/* ОТКАТ: colSpan должен быть 4 вместо 2 */}
|
||||
<TableCell colSpan={4} className="text-right pr-8">
|
||||
{/* Агрегированные данные поставщика отображаются только в итого */}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{formatCurrency(aggregatedData.total)}
|
||||
</span>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
@ -1362,7 +1523,7 @@ export function MultiLevelSuppliesTable({
|
||||
<TableCell className="text-white/70 font-mono">{size.quantity}</TableCell>
|
||||
<TableCell
|
||||
className="text-white/60 font-mono"
|
||||
colSpan={userRole === 'WHOLESALE' ? 3 : 7}
|
||||
colSpan={userRole === 'WHOLESALE' ? 2 : 7}
|
||||
>
|
||||
{size.price ? formatCurrency(size.price) : '-'}
|
||||
</TableCell>
|
||||
@ -1377,10 +1538,132 @@ export function MultiLevelSuppliesTable({
|
||||
})
|
||||
})()}
|
||||
|
||||
{/* УРОВЕНЬ 3: Поставщик для WHOLESALE (без маршрутов) - СКРЫТ */}
|
||||
{false && isSupplyExpanded && userRole === 'WHOLESALE' && (
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors"
|
||||
style={{ backgroundColor: getLevelBackgroundColor(3, index) }}
|
||||
onClick={() => toggleSupplierExpansion(supply.partner.id)}
|
||||
>
|
||||
<TableCell className="relative">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1 h-1 rounded-full bg-green-400 mr-1"></div>
|
||||
<Building2 className="h-3 w-3 text-green-400" />
|
||||
<span className="text-white font-medium text-sm">Поставщик</span>
|
||||
</div>
|
||||
<div
|
||||
className="absolute left-0 top-0 w-0.5 h-full"
|
||||
style={{ backgroundColor: getSupplyColor(index) }}
|
||||
></div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white text-sm font-medium">
|
||||
{supply.partner.name || supply.partner.fullName}
|
||||
</span>
|
||||
{supply.partner.users &&
|
||||
supply.partner.users.length > 0 &&
|
||||
supply.partner.users[0].managerName && (
|
||||
<span className="text-white/60 text-xs">
|
||||
{supply.partner.users[0].managerName}
|
||||
</span>
|
||||
)}
|
||||
{supply.partner.phones &&
|
||||
Array.isArray(supply.partner.phones) &&
|
||||
supply.partner.phones.length > 0 && (
|
||||
<span className="text-white/60 text-[10px]">
|
||||
{typeof supply.partner.phones[0] === 'string'
|
||||
? supply.partner.phones[0]
|
||||
: supply.partner.phones[0]?.value || supply.partner.phones[0]?.phone}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.orderedTotal}</span>
|
||||
</TableCell>
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE не видит колонки "Поставлено" и "Брак" на supplier level */}
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.deliveredTotal}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">{aggregatedData.defectTotal}</span>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
<TableCell>
|
||||
<span className="text-green-400 font-medium text-sm">
|
||||
{formatCurrency(aggregatedData.goodsPrice)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{/* УРОВЕНЬ 4: Товары для WHOLESALE (без маршрутов) */}
|
||||
{isSupplyExpanded &&
|
||||
userRole === 'WHOLESALE' &&
|
||||
(supply.items || []).map((item) => {
|
||||
const isProductExpanded = expandedProducts.has(item.id)
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors"
|
||||
style={{ backgroundColor: getLevelBackgroundColor(4, index) }}
|
||||
onClick={() => toggleProductExpansion(item.id)}
|
||||
>
|
||||
<TableCell className="relative">
|
||||
<div
|
||||
className="absolute left-0 top-0 w-0.5 h-full"
|
||||
style={{ backgroundColor: getSupplyColor(index) }}
|
||||
></div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white text-sm font-medium">{item.product.name}</span>
|
||||
<span className="text-white/60 text-[9px]">
|
||||
Арт: {item.product.article || 'SF-T-925635-494'}
|
||||
{item.product.category && ` · ${item.product.category.name}`}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">{item.quantity}</span>
|
||||
</TableCell>
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: WHOLESALE не видит колонки "Поставлено" и "Брак" на product level */}
|
||||
{userRole !== 'WHOLESALE' && (
|
||||
<>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">0</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">0</span>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
<TableCell>
|
||||
<div className="text-white">
|
||||
<div className="font-medium text-sm">{formatCurrency(item.totalPrice)}</div>
|
||||
<div className="text-xs text-white/60 hidden sm:block">
|
||||
{formatCurrency(item.price)} за шт.
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell> </TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* ВАРИАНТ 1: Разделитель в виде пустой строки с border */}
|
||||
<tr>
|
||||
{/* 🔒 БЕЗОПАСНОСТЬ: colSpan учитывает скрытые колонки для WHOLESALE + новые колонки объём/грузовые места */}
|
||||
<td
|
||||
colSpan={userRole === 'WHOLESALE' ? 8 : 12}
|
||||
colSpan={userRole === 'WHOLESALE' ? 7 : 9}
|
||||
style={{ padding: 0, borderBottom: '1px solid rgba(255, 255, 255, 0.2)' }}
|
||||
></td>
|
||||
</tr>
|
||||
|
Reference in New Issue
Block a user