Compare commits
2 Commits
c5cd75cdb9
...
54ee8e4f79
Author | SHA1 | Date | |
---|---|---|---|
54ee8e4f79 | |||
d41ad618c7 |
@ -9,11 +9,13 @@
|
|||||||
## 📋 АКТИВНЫЕ ЗАДАЧИ
|
## 📋 АКТИВНЫЕ ЗАДАЧИ
|
||||||
|
|
||||||
### Текущая задача:
|
### Текущая задача:
|
||||||
|
|
||||||
- **Что делаем**: ✅ Унификация UI раздела "Партнеры" (ЗАВЕРШЕНО)
|
- **Что делаем**: ✅ Унификация UI раздела "Партнеры" (ЗАВЕРШЕНО)
|
||||||
- **Статус**: Завершена
|
- **Статус**: Завершена
|
||||||
- **Начато**: 2025-08-11
|
- **Начато**: 2025-08-11
|
||||||
|
|
||||||
### Завершенные задачи:
|
### Завершенные задачи:
|
||||||
|
|
||||||
1. ✅ Восстановить rules-complete.md из backup
|
1. ✅ Восстановить rules-complete.md из backup
|
||||||
2. ✅ Создать систему сохранения контекста
|
2. ✅ Создать систему сохранения контекста
|
||||||
3. ✅ Исправить React Hooks ошибки в sidebar.tsx
|
3. ✅ Исправить React Hooks ошибки в sidebar.tsx
|
||||||
@ -22,6 +24,7 @@
|
|||||||
6. ✅ Обновить правила в partners-rules.md и visual-design-rules.md
|
6. ✅ Обновить правила в partners-rules.md и visual-design-rules.md
|
||||||
|
|
||||||
### Очередь задач:
|
### Очередь задач:
|
||||||
|
|
||||||
1. ⏳ [Ожидание новых задач от пользователя]
|
1. ⏳ [Ожидание новых задач от пользователя]
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -29,26 +32,31 @@
|
|||||||
## 🔧 ТЕКУЩИЙ КОНТЕКСТ ПРОЕКТА
|
## 🔧 ТЕКУЩИЙ КОНТЕКСТ ПРОЕКТА
|
||||||
|
|
||||||
### О проекте SFERA:
|
### О проекте SFERA:
|
||||||
|
|
||||||
**Тип**: Система управления складами и поставками (B2B маркетплейс)
|
**Тип**: Система управления складами и поставками (B2B маркетплейс)
|
||||||
**Технологии**:
|
**Технологии**:
|
||||||
|
|
||||||
- Frontend: Next.js 15.4.1 (React 19), TypeScript, Tailwind CSS
|
- Frontend: Next.js 15.4.1 (React 19), TypeScript, Tailwind CSS
|
||||||
- Backend: GraphQL (Apollo Server), Prisma ORM
|
- Backend: GraphQL (Apollo Server), Prisma ORM
|
||||||
- База данных: PostgreSQL (через Prisma)
|
- База данных: PostgreSQL (через Prisma)
|
||||||
- UI: Radix UI, Lucide icons, shadcn/ui компоненты
|
- UI: Radix UI, Lucide icons, shadcn/ui компоненты
|
||||||
|
|
||||||
### Архитектура:
|
### Архитектура:
|
||||||
|
|
||||||
- **4 типа кабинетов**: SELLER (селлер), FULFILLMENT (фулфилмент), WHOLESALE (поставщик), LOGIST (логистика)
|
- **4 типа кабинетов**: SELLER (селлер), FULFILLMENT (фулфилмент), WHOLESALE (поставщик), LOGIST (логистика)
|
||||||
- **Типы предметов**: PRODUCT (товар), CONSUMABLE (расходники), DEFECT (брак), FINISHED_PRODUCT (готовый продукт)
|
- **Типы предметов**: PRODUCT (товар), CONSUMABLE (расходники), DEFECT (брак), FINISHED_PRODUCT (готовый продукт)
|
||||||
- **Workflow поставок**: 8 статусов от PENDING до DELIVERED
|
- **Workflow поставок**: 8 статусов от PENDING до DELIVERED
|
||||||
- **Система партнерства**: через модель Counterparty
|
- **Система партнерства**: через модель Counterparty
|
||||||
|
|
||||||
### Ключевые особенности:
|
### Ключевые особенности:
|
||||||
|
|
||||||
- Строгая типизация GraphQL + TypeScript
|
- Строгая типизация GraphQL + TypeScript
|
||||||
- Ролевая модель доступа (проверки на уровне резолверов)
|
- Ролевая модель доступа (проверки на уровне резолверов)
|
||||||
- Модульная структура компонентов по кабинетам
|
- Модульная структура компонентов по кабинетам
|
||||||
- Glass-эффекты и OKLCH цветовая система в UI
|
- Glass-эффекты и OKLCH цветовая система в UI
|
||||||
|
|
||||||
### Важные решения:
|
### Важные решения:
|
||||||
|
|
||||||
- Восстановлен файл rules-complete.md из backup-20250809-192625 (3,301 строк)
|
- Восстановлен файл rules-complete.md из backup-20250809-192625 (3,301 строк)
|
||||||
- Удалена испорченная версия (2,686 строк)
|
- Удалена испорченная версия (2,686 строк)
|
||||||
- Создана система сохранения контекста (current-session.md, task-template.md)
|
- Создана система сохранения контекста (current-session.md, task-template.md)
|
||||||
@ -57,22 +65,26 @@
|
|||||||
- **2025-08-11**: Установлена единая цветовая схема для реферальных/партнерских ссылок (желтая)
|
- **2025-08-11**: Установлена единая цветовая схема для реферальных/партнерских ссылок (желтая)
|
||||||
|
|
||||||
### Обнаруженные проблемы:
|
### Обнаруженные проблемы:
|
||||||
|
|
||||||
- ✅ **Решено**: Claude часто теряет контекст при длинных сессиях → создана система current-session.md
|
- ✅ **Решено**: Claude часто теряет контекст при длинных сессиях → создана система current-session.md
|
||||||
- ✅ **Решено**: React Hooks вызывались после условного return в sidebar.tsx → хуки перенесены в начало компонента
|
- ✅ **Решено**: React Hooks вызывались после условного return в sidebar.tsx → хуки перенесены в начало компонента
|
||||||
- ✅ **Решено**: Блоки статистики в контрагентах были непрозрачными → убрана лишняя обертка glass-card
|
- ✅ **Решено**: Блоки статистики в контрагентах были непрозрачными → убрана лишняя обертка glass-card
|
||||||
- ✅ **Решено**: Разная цветовая схема между вкладками → унифицирована желтая схема для ссылок
|
- ✅ **Решено**: Разная цветовая схема между вкладками → унифицирована желтая схема для ссылок
|
||||||
|
|
||||||
### Согласованные подходы:
|
### Согласованные подходы:
|
||||||
|
|
||||||
- Использовать TodoWrite для планирования
|
- Использовать TodoWrite для планирования
|
||||||
- Документировать все важные решения
|
- Документировать все важные решения
|
||||||
- Следовать правилам из interaction-integrity-rules.md
|
- Следовать правилам из interaction-integrity-rules.md
|
||||||
- Всегда читать rules-complete.md перед изменениями
|
- Всегда читать rules-complete.md перед изменениями
|
||||||
|
- **ВСЕГДА ПРИМЕНЯТЬ ТОЛЬКО БЕЗОПАСНЫЕ ИСПРАВЛЕНИЯ** (добавлено 2025-08-12)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💡 ВАЖНЫЕ ОТКРЫТИЯ И РЕШЕНИЯ
|
## 💡 ВАЖНЫЕ ОТКРЫТИЯ И РЕШЕНИЯ
|
||||||
|
|
||||||
### Структура правил системы:
|
### Структура правил системы:
|
||||||
|
|
||||||
- `rules-complete.md` - основные бизнес-правила
|
- `rules-complete.md` - основные бизнес-правила
|
||||||
- `interaction-integrity-rules.md` - методология работы Claude
|
- `interaction-integrity-rules.md` - методология работы Claude
|
||||||
- `CLAUDE.md` - системные правила и напоминания
|
- `CLAUDE.md` - системные правила и напоминания
|
||||||
@ -81,6 +93,7 @@
|
|||||||
- `visual-design-rules.md` - общие визуальные правила + унификация интерфейсов
|
- `visual-design-rules.md` - общие визуальные правила + унификация интерфейсов
|
||||||
|
|
||||||
### Критические открытия 2025-08-11:
|
### Критические открытия 2025-08-11:
|
||||||
|
|
||||||
- **DOM структура влияет на прозрачность**: Вложенные `glass-card` создают непрозрачность
|
- **DOM структура влияет на прозрачность**: Вложенные `glass-card` создают непрозрачность
|
||||||
- **Цвета должны быть консистентными**: Аналогичные элементы = одинаковая цветовая схема
|
- **Цвета должны быть консистентными**: Аналогичные элементы = одинаковая цветовая схема
|
||||||
- **TabsContent обертки опасны**: Лишние контейнеры ломают glass-morphism эффекты
|
- **TabsContent обертки опасны**: Лишние контейнеры ломают glass-morphism эффекты
|
||||||
@ -119,7 +132,9 @@ npm run dev
|
|||||||
## 🔄 ИСТОРИЯ ИЗМЕНЕНИЙ
|
## 🔄 ИСТОРИЯ ИЗМЕНЕНИЙ
|
||||||
|
|
||||||
### 2025-08-11 🎨 УНИФИКАЦИЯ UI РАЗДЕЛА "ПАРТНЕРЫ"
|
### 2025-08-11 🎨 УНИФИКАЦИЯ UI РАЗДЕЛА "ПАРТНЕРЫ"
|
||||||
|
|
||||||
#### ✅ Выполнено:
|
#### ✅ Выполнено:
|
||||||
|
|
||||||
- **Исправлены React Hooks ошибки** в `src/components/dashboard/sidebar.tsx`
|
- **Исправлены React Hooks ошибки** в `src/components/dashboard/sidebar.tsx`
|
||||||
- **Полная унификация визуала** вкладок "Рефералы" и "Мои контрагенты"
|
- **Полная унификация визуала** вкладок "Рефералы" и "Мои контрагенты"
|
||||||
- **Оптимизировано пространство** в интерфейсе (уменьшены отступы и размеры)
|
- **Оптимизировано пространство** в интерфейсе (уменьшены отступы и размеры)
|
||||||
@ -128,12 +143,14 @@ npm run dev
|
|||||||
- **Убрана лишняя обертка** `glass-card` в `partners-dashboard.tsx`
|
- **Убрана лишняя обертка** `glass-card` в `partners-dashboard.tsx`
|
||||||
|
|
||||||
#### 🐛 Исправленные баги:
|
#### 🐛 Исправленные баги:
|
||||||
|
|
||||||
- Хуки вызывались после условного return → перенесены в начало компонента
|
- Хуки вызывались после условного return → перенесены в начало компонента
|
||||||
- Блоки статистики были непрозрачными → убрана лишняя DOM обертка
|
- Блоки статистики были непрозрачными → убрана лишняя DOM обертка
|
||||||
- Неправильная цветовая схема → унифицирована желтая схема
|
- Неправильная цветовая схема → унифицирована желтая схема
|
||||||
- Проблемы с hot reload → перезапуск сервера с очисткой кэша
|
- Проблемы с hot reload → перезапуск сервера с очисткой кэша
|
||||||
|
|
||||||
#### 📁 Измененные файлы:
|
#### 📁 Измененные файлы:
|
||||||
|
|
||||||
- `src/components/dashboard/sidebar.tsx` - исправлены React Hooks Rules
|
- `src/components/dashboard/sidebar.tsx` - исправлены React Hooks Rules
|
||||||
- `src/components/market/market-counterparties.tsx` - унификация структуры
|
- `src/components/market/market-counterparties.tsx` - унификация структуры
|
||||||
- `src/components/partners/partners-dashboard.tsx` - убрана лишняя обертка
|
- `src/components/partners/partners-dashboard.tsx` - убрана лишняя обертка
|
||||||
@ -142,12 +159,14 @@ npm run dev
|
|||||||
- `visual-design-rules.md` - добавлены правила унификации интерфейсов
|
- `visual-design-rules.md` - добавлены правила унификации интерфейсов
|
||||||
|
|
||||||
#### 📋 Результат:
|
#### 📋 Результат:
|
||||||
|
|
||||||
- **Идентичный визуал** всех вкладок раздела "Партнеры"
|
- **Идентичный визуал** всех вкладок раздела "Партнеры"
|
||||||
- **Правильная прозрачность** glass-morphism эффектов
|
- **Правильная прозрачность** glass-morphism эффектов
|
||||||
- **Единая цветовая схема** для аналогичных элементов
|
- **Единая цветовая схема** для аналогичных элементов
|
||||||
- **Зафиксированные правила** в документации для будущего
|
- **Зафиксированные правила** в документации для будущего
|
||||||
|
|
||||||
### 2025-08-10
|
### 2025-08-10
|
||||||
|
|
||||||
- Создан файл current-session.md
|
- Создан файл current-session.md
|
||||||
- Восстановлен rules-complete.md из резервной копии
|
- Восстановлен rules-complete.md из резервной копии
|
||||||
- Начата работа над системой сохранения контекста
|
- Начата работа над системой сохранения контекста
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
- ✅ **Исправлять ошибки, а не обходить их** - каждая ошибка ESLint должна быть исправлена
|
- ✅ **Исправлять ошибки, а не обходить их** - каждая ошибка ESLint должна быть исправлена
|
||||||
- ✅ **Обход проверок создает технический долг** - `--no-verify` использовать только в крайних случаях
|
- ✅ **Обход проверок создает технический долг** - `--no-verify` использовать только в крайних случаях
|
||||||
- ✅ **Лучше потратить время на исправление, чем накапливать проблемы** - долгосрочная перспектива важнее
|
- ✅ **Лучше потратить время на исправление, чем накапливать проблемы** - долгосрочная перспектива важнее
|
||||||
|
- ✅ **ВСЕГДА ПРИМЕНЯТЬ ТОЛЬКО БЕЗОПАСНЫЕ ИСПРАВЛЕНИЯ** - никаких рискованных изменений без явного согласия
|
||||||
|
|
||||||
**ПРИ ОШИБКАХ ЛИНТЕРА:**
|
**ПРИ ОШИБКАХ ЛИНТЕРА:**
|
||||||
|
|
||||||
@ -606,6 +607,7 @@ ignores: ['diagnostic-script.js', 'legacy-config.js'] // конкретные ф
|
|||||||
- ✅ Качественное выполнение задач
|
- ✅ Качественное выполнение задач
|
||||||
- ✅ Предотвращение ошибок и недопонимания
|
- ✅ Предотвращение ошибок и недопонимания
|
||||||
- ✅ Соблюдение архитектуры и правил системы
|
- ✅ Соблюдение архитектуры и правил системы
|
||||||
|
- ✅ **БЕЗОПАСНОСТЬ ИЗМЕНЕНИЙ** - защита от рискованных модификаций
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Package, Plus, Minus, X, FileText, Settings, ShoppingCart } from 'lucide-react'
|
import { Package, Plus, Minus, X, Settings, ShoppingCart } from 'lucide-react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
@ -1,15 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useMutation, useQuery } from '@apollo/client'
|
import { useMutation, useQuery } from '@apollo/client'
|
||||||
import {
|
import { ArrowLeft, Building2, Package, Plus, Search, ShoppingCart, X } from 'lucide-react'
|
||||||
ArrowLeft,
|
|
||||||
Building2,
|
|
||||||
Package,
|
|
||||||
Plus,
|
|
||||||
Search,
|
|
||||||
ShoppingCart,
|
|
||||||
X,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
@ -24,11 +16,11 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { CREATE_SUPPLY_ORDER } from '@/graphql/mutations'
|
import { CREATE_SUPPLY_ORDER } from '@/graphql/mutations'
|
||||||
import {
|
import {
|
||||||
GET_AVAILABLE_SUPPLIES_FOR_RECIPE,
|
GET_AVAILABLE_SUPPLIES_FOR_RECIPE,
|
||||||
GET_COUNTERPARTY_SERVICES,
|
GET_COUNTERPARTY_SERVICES,
|
||||||
GET_COUNTERPARTY_SUPPLIES,
|
GET_COUNTERPARTY_SUPPLIES,
|
||||||
GET_MY_COUNTERPARTIES,
|
GET_MY_COUNTERPARTIES,
|
||||||
GET_ORGANIZATION_PRODUCTS,
|
GET_ORGANIZATION_PRODUCTS,
|
||||||
} from '@/graphql/queries'
|
} from '@/graphql/queries'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import { useSidebar } from '@/hooks/useSidebar'
|
import { useSidebar } from '@/hooks/useSidebar'
|
||||||
@ -90,7 +82,7 @@ interface SelectedGoodsItem {
|
|||||||
parameters?: Array<{ name: string; value: string }> // Параметры товара
|
parameters?: Array<{ name: string; value: string }> // Параметры товара
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LogisticsCompany {
|
interface _LogisticsCompany {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
estimatedCost: number
|
estimatedCost: number
|
||||||
@ -272,7 +264,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
const _wbCards: WBCard[] = [] // Временно отключено
|
const _wbCards: WBCard[] = [] // Временно отключено
|
||||||
|
|
||||||
// Показываем только партнеров с типом WHOLESALE согласно rules2.md 13.3
|
// Показываем только партнеров с типом WHOLESALE согласно rules2.md 13.3
|
||||||
const wholesaleSuppliers = allCounterparties.filter((cp: any) => {
|
const wholesaleSuppliers = allCounterparties.filter((cp: GoodsSupplier) => {
|
||||||
try {
|
try {
|
||||||
return cp && cp.type === 'WHOLESALE'
|
return cp && cp.type === 'WHOLESALE'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -301,7 +293,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
|
|
||||||
// Получаем товары выбранного поставщика согласно rules2.md 13.3
|
// Получаем товары выбранного поставщика согласно rules2.md 13.3
|
||||||
// Теперь фильтрация происходит на сервере через GraphQL запрос
|
// Теперь фильтрация происходит на сервере через GraphQL запрос
|
||||||
const products = (productsData?.organizationProducts || []).filter((product: any) => {
|
const products = (productsData?.organizationProducts || []).filter((product: GoodsProduct) => {
|
||||||
try {
|
try {
|
||||||
return product && product.id && product.name
|
return product && product.id && product.name
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -357,7 +349,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
const logisticsCompanies = allCounterparties?.filter((partner) => partner.type === 'LOGIST') || []
|
const logisticsCompanies = allCounterparties?.filter((partner) => partner.type === 'LOGIST') || []
|
||||||
|
|
||||||
// Моковые фулфилмент-центры согласно rules2.md 9.7.2
|
// Моковые фулфилмент-центры согласно rules2.md 9.7.2
|
||||||
const fulfillmentCenters = [
|
const _fulfillmentCenters = [
|
||||||
{ id: 'ff1', name: 'СФ Центр Москва', address: 'г. Москва, ул. Складская 10' },
|
{ id: 'ff1', name: 'СФ Центр Москва', address: 'г. Москва, ул. Складская 10' },
|
||||||
{ id: 'ff2', name: 'СФ Центр СПб', address: 'г. Санкт-Петербург, пр. Логистический 5' },
|
{ id: 'ff2', name: 'СФ Центр СПб', address: 'г. Санкт-Петербург, пр. Логистический 5' },
|
||||||
{ id: 'ff3', name: 'СФ Центр Екатеринбург', address: 'г. Екатеринбург, ул. Промышленная 15' },
|
{ id: 'ff3', name: 'СФ Центр Екатеринбург', address: 'г. Екатеринбург, ул. Промышленная 15' },
|
||||||
@ -378,7 +370,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
// Removed unused updateProductQuantity function
|
// Removed unused updateProductQuantity function
|
||||||
|
|
||||||
// Добавление товара в корзину из карточки с заданным количеством
|
// Добавление товара в корзину из карточки с заданным количеством
|
||||||
const addToCart = (product: GoodsProduct) => {
|
const _addToCart = (product: GoodsProduct) => {
|
||||||
const quantity = getProductQuantity(product.id)
|
const quantity = getProductQuantity(product.id)
|
||||||
if (quantity <= 0) {
|
if (quantity <= 0) {
|
||||||
toast.error('Укажите количество товара')
|
toast.error('Укажите количество товара')
|
||||||
@ -513,7 +505,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Расчет стоимости компонентов рецептуры
|
// Расчет стоимости компонентов рецептуры
|
||||||
const calculateRecipeCost = (productId: string) => {
|
const _calculateRecipeCost = (productId: string) => {
|
||||||
const recipe = productRecipes[productId]
|
const recipe = productRecipes[productId]
|
||||||
if (!recipe) return { services: 0, consumables: 0, total: 0 }
|
if (!recipe) return { services: 0, consumables: 0, total: 0 }
|
||||||
|
|
||||||
@ -582,8 +574,8 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
selectedQuantity: quantity,
|
selectedQuantity: quantity,
|
||||||
unit: product.unit,
|
unit: product.unit,
|
||||||
category: product.category?.name,
|
category: product.category?.name,
|
||||||
supplierId: selectedSupplier!.id,
|
supplierId: selectedSupplier?.id || '',
|
||||||
supplierName: selectedSupplier!.name || selectedSupplier!.fullName || '',
|
supplierName: selectedSupplier?.name || selectedSupplier?.fullName || '',
|
||||||
completeness: additionalData?.completeness,
|
completeness: additionalData?.completeness,
|
||||||
recipe: additionalData?.recipe,
|
recipe: additionalData?.recipe,
|
||||||
specialRequirements: additionalData?.specialRequirements,
|
specialRequirements: additionalData?.specialRequirements,
|
||||||
@ -610,7 +602,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
|
|
||||||
// Функция расчета полной стоимости товара с рецептурой
|
// Функция расчета полной стоимости товара с рецептурой
|
||||||
const getProductTotalWithRecipe = (productId: string, quantity: number) => {
|
const getProductTotalWithRecipe = (productId: string, quantity: number) => {
|
||||||
const product = allSelectedProducts.find(p => p.id === productId)
|
const product = allSelectedProducts.find((p) => p.id === productId)
|
||||||
if (!product) return 0
|
if (!product) return 0
|
||||||
|
|
||||||
const baseTotal = product.price * quantity
|
const baseTotal = product.price * quantity
|
||||||
@ -620,20 +612,20 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
|
|
||||||
// Услуги ФФ
|
// Услуги ФФ
|
||||||
const servicesCost = (recipe.selectedServices || []).reduce((sum, serviceId) => {
|
const servicesCost = (recipe.selectedServices || []).reduce((sum, serviceId) => {
|
||||||
const service = fulfillmentServices.find(s => s.id === serviceId)
|
const service = fulfillmentServices.find((s) => s.id === serviceId)
|
||||||
return sum + (service ? service.price * quantity : 0)
|
return sum + (service ? service.price * quantity : 0)
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
// Расходники ФФ
|
// Расходники ФФ
|
||||||
const ffConsumablesCost = (recipe.selectedFFConsumables || []).reduce((sum, consumableId) => {
|
const ffConsumablesCost = (recipe.selectedFFConsumables || []).reduce((sum, consumableId) => {
|
||||||
const consumable = fulfillmentConsumables.find(c => c.id === consumableId)
|
const consumable = fulfillmentConsumables.find((c) => c.id === consumableId)
|
||||||
// Используем такую же логику как в карточке - только price
|
// Используем такую же логику как в карточке - только price
|
||||||
return sum + (consumable ? consumable.price * quantity : 0)
|
return sum + (consumable ? consumable.price * quantity : 0)
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
// Расходники селлера
|
// Расходники селлера
|
||||||
const sellerConsumablesCost = (recipe.selectedSellerConsumables || []).reduce((sum, consumableId) => {
|
const sellerConsumablesCost = (recipe.selectedSellerConsumables || []).reduce((sum, consumableId) => {
|
||||||
const consumable = sellerConsumables.find(c => c.id === consumableId)
|
const consumable = sellerConsumables.find((c) => c.id === consumableId)
|
||||||
return sum + (consumable ? (consumable.pricePerUnit || 0) * quantity : 0)
|
return sum + (consumable ? (consumable.pricePerUnit || 0) * quantity : 0)
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
@ -645,13 +637,14 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
return sum + getProductTotalWithRecipe(item.id, item.selectedQuantity)
|
return sum + getProductTotalWithRecipe(item.id, item.selectedQuantity)
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
const totalQuantity = selectedGoods.reduce((sum, item) => sum + item.selectedQuantity, 0)
|
const _totalQuantity = selectedGoods.reduce((sum, item) => sum + item.selectedQuantity, 0)
|
||||||
const totalAmount = totalGoodsAmount
|
const totalAmount = totalGoodsAmount
|
||||||
|
|
||||||
// Валидация формы согласно rules2.md 9.7.6
|
// Валидация формы согласно rules2.md 9.7.6
|
||||||
// Проверяем обязательность услуг фулфилмента согласно rules-complete.md
|
// Проверяем обязательность услуг фулфилмента согласно rules-complete.md
|
||||||
const hasRequiredServices = selectedGoods.every((item) => productRecipes[item.id]?.selectedServices?.length > 0)
|
const hasRequiredServices = selectedGoods.every((item) => productRecipes[item.id]?.selectedServices?.length > 0)
|
||||||
|
|
||||||
|
// Проверка валидности формы - все обязательные поля заполнены
|
||||||
const isFormValid =
|
const isFormValid =
|
||||||
selectedSupplier && selectedGoods.length > 0 && deliveryDate && selectedFulfillment && hasRequiredServices // Обязательно: каждый товар должен иметь услуги
|
selectedSupplier && selectedGoods.length > 0 && deliveryDate && selectedFulfillment && hasRequiredServices // Обязательно: каждый товар должен иметь услуги
|
||||||
|
|
||||||
@ -670,7 +663,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
try {
|
try {
|
||||||
await createSupplyOrder({
|
await createSupplyOrder({
|
||||||
variables: {
|
variables: {
|
||||||
supplierId: selectedSupplier!.id,
|
supplierId: selectedSupplier?.id || '',
|
||||||
fulfillmentCenterId: selectedFulfillment,
|
fulfillmentCenterId: selectedFulfillment,
|
||||||
items: selectedGoods.map((item) => ({
|
items: selectedGoods.map((item) => ({
|
||||||
productId: item.id,
|
productId: item.id,
|
||||||
@ -707,8 +700,8 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
const maxDate = new Date()
|
const maxDate = new Date()
|
||||||
maxDate.setDate(maxDate.getDate() + 90)
|
maxDate.setDate(maxDate.getDate() + 90)
|
||||||
|
|
||||||
const minDateString = tomorrow.toISOString().split('T')[0]
|
const _minDateString = tomorrow.toISOString().split('T')[0]
|
||||||
const maxDateString = maxDate.toISOString().split('T')[0]
|
const _maxDateString = maxDate.toISOString().split('T')[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex overflow-hidden">
|
<div className="h-screen flex overflow-hidden">
|
||||||
@ -816,10 +809,10 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
className={`flex-shrink-0 p-3 rounded-lg cursor-pointer group transition-all duration-200
|
className={`flex-shrink-0 p-3 rounded-lg cursor-pointer group transition-all duration-200
|
||||||
w-[184px] md:w-[200px] lg:w-[216px] h-[92px]
|
w-[184px] md:w-[200px] lg:w-[216px] h-[92px]
|
||||||
${
|
${
|
||||||
selectedSupplier?.id === supplier.id
|
selectedSupplier?.id === supplier.id
|
||||||
? 'bg-green-500/20 border border-green-400/60 shadow-lg ring-1 ring-green-400/30'
|
? 'bg-green-500/20 border border-green-400/60 shadow-lg ring-1 ring-green-400/30'
|
||||||
: 'bg-white/5 border border-white/10 hover:border-white/20 hover:bg-white/10 hover:shadow-md'
|
: 'bg-white/5 border border-white/10 hover:border-white/20 hover:bg-white/10 hover:shadow-md'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2 h-full">
|
<div className="flex items-start gap-2 h-full">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
@ -993,12 +986,15 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
|
|
||||||
// Общая стоимость товара с рецептурой
|
// Общая стоимость товара с рецептурой
|
||||||
const totalWithRecipe =
|
const totalWithRecipe =
|
||||||
product.price * product.selectedQuantity + servicesCost + ffConsumablesCost + sellerConsumablesCost
|
product.price * product.selectedQuantity +
|
||||||
|
servicesCost +
|
||||||
|
ffConsumablesCost +
|
||||||
|
sellerConsumablesCost
|
||||||
|
|
||||||
// Debug: сравниваем с функцией расчета корзины
|
// Debug: сравниваем с функцией расчета корзины
|
||||||
const cartTotal = getProductTotalWithRecipe(product.id, product.selectedQuantity)
|
const cartTotal = getProductTotalWithRecipe(product.id, product.selectedQuantity)
|
||||||
if (Math.abs(totalWithRecipe - cartTotal) > 0.01) {
|
if (Math.abs(totalWithRecipe - cartTotal) > 0.01) {
|
||||||
console.log(`РАЗНИЦА для ${product.name}:`, {
|
console.warn(`Расхождение в расчете для ${product.name}:`, {
|
||||||
карточка: totalWithRecipe,
|
карточка: totalWithRecipe,
|
||||||
корзина: cartTotal,
|
корзина: cartTotal,
|
||||||
базовая_цена: product.price * product.selectedQuantity,
|
базовая_цена: product.price * product.selectedQuantity,
|
||||||
@ -1076,7 +1072,9 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full ${product.quantity > 0 ? 'bg-green-400' : 'bg-red-400'}`}
|
className={`w-2 h-2 rounded-full ${product.quantity > 0 ? 'bg-green-400' : 'bg-red-400'}`}
|
||||||
></div>
|
></div>
|
||||||
<span className={`text-xs ${product.quantity > 0 ? 'text-green-400' : 'text-red-400'}`}>
|
<span
|
||||||
|
className={`text-xs ${product.quantity > 0 ? 'text-green-400' : 'text-red-400'}`}
|
||||||
|
>
|
||||||
{product.quantity > 0 ? `${product.quantity} шт` : 'Нет в наличии'}
|
{product.quantity > 0 ? `${product.quantity} шт` : 'Нет в наличии'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -1090,7 +1088,8 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
value={product.selectedQuantity || ''}
|
value={product.selectedQuantity || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const inputValue = e.target.value
|
const inputValue = e.target.value
|
||||||
const newQuantity = inputValue === '' ? 0 : Math.max(0, parseInt(inputValue) || 0)
|
const newQuantity =
|
||||||
|
inputValue === '' ? 0 : Math.max(0, parseInt(inputValue) || 0)
|
||||||
setAllSelectedProducts((prev) =>
|
setAllSelectedProducts((prev) =>
|
||||||
prev.map((p) =>
|
prev.map((p) =>
|
||||||
p.id === product.id ? { ...p, selectedQuantity: newQuantity } : p,
|
p.id === product.id ? { ...p, selectedQuantity: newQuantity } : p,
|
||||||
@ -1100,26 +1099,30 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
// Автоматическое добавление/удаление из корзины
|
// Автоматическое добавление/удаление из корзины
|
||||||
if (newQuantity > 0) {
|
if (newQuantity > 0) {
|
||||||
// Добавляем в корзину
|
// Добавляем в корзину
|
||||||
const existingItem = selectedGoods.find(item => item.id === product.id)
|
const existingItem = selectedGoods.find((item) => item.id === product.id)
|
||||||
if (!existingItem) {
|
if (!existingItem) {
|
||||||
// Добавляем новый товар
|
// Добавляем новый товар
|
||||||
setSelectedGoods(prev => [...prev, {
|
setSelectedGoods((prev) => [
|
||||||
id: product.id,
|
...prev,
|
||||||
name: product.name,
|
{
|
||||||
sku: product.article,
|
id: product.id,
|
||||||
price: product.price,
|
name: product.name,
|
||||||
category: product.category?.name || '',
|
sku: product.article,
|
||||||
selectedQuantity: newQuantity,
|
price: product.price,
|
||||||
unit: product.unit || 'шт',
|
category: product.category?.name || '',
|
||||||
supplierId: selectedSupplier?.id || '',
|
selectedQuantity: newQuantity,
|
||||||
supplierName: selectedSupplier?.name || selectedSupplier?.fullName || 'Поставщик',
|
unit: product.unit || 'шт',
|
||||||
}])
|
supplierId: selectedSupplier?.id || '',
|
||||||
|
supplierName:
|
||||||
|
selectedSupplier?.name || selectedSupplier?.fullName || 'Поставщик',
|
||||||
|
},
|
||||||
|
])
|
||||||
// Инициализируем рецептуру
|
// Инициализируем рецептуру
|
||||||
initializeProductRecipe(product.id)
|
initializeProductRecipe(product.id)
|
||||||
} else {
|
} else {
|
||||||
// Обновляем количество
|
// Обновляем количество
|
||||||
setSelectedGoods(prev =>
|
setSelectedGoods((prev) =>
|
||||||
prev.map(item =>
|
prev.map((item) =>
|
||||||
item.id === product.id
|
item.id === product.id
|
||||||
? { ...item, selectedQuantity: newQuantity }
|
? { ...item, selectedQuantity: newQuantity }
|
||||||
: item,
|
: item,
|
||||||
@ -1128,7 +1131,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Удаляем из корзины при количестве 0
|
// Удаляем из корзины при количестве 0
|
||||||
setSelectedGoods(prev => prev.filter(item => item.id !== product.id))
|
setSelectedGoods((prev) => prev.filter((item) => item.id !== product.id))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="glass-input w-16 h-8 text-sm text-center text-white placeholder:text-white/50"
|
className="glass-input w-16 h-8 text-sm text-center text-white placeholder:text-white/50"
|
||||||
@ -1151,7 +1154,9 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
{servicesCost.toLocaleString('ru-RU')} ₽
|
{servicesCost.toLocaleString('ru-RU')} ₽
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h6 className="text-purple-400 text-xs font-medium uppercase tracking-wider">🛠️ Услуги ФФ</h6>
|
<h6 className="text-purple-400 text-xs font-medium uppercase tracking-wider">
|
||||||
|
🛠️ Услуги ФФ
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
|
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
|
||||||
{fulfillmentServices.length > 0 ? (
|
{fulfillmentServices.length > 0 ? (
|
||||||
@ -1197,7 +1202,9 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
{ffConsumablesCost.toLocaleString('ru-RU')} ₽
|
{ffConsumablesCost.toLocaleString('ru-RU')} ₽
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h6 className="text-orange-400 text-xs font-medium uppercase tracking-wider">📦 Расходники ФФ</h6>
|
<h6 className="text-orange-400 text-xs font-medium uppercase tracking-wider">
|
||||||
|
📦 Расходники ФФ
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
|
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
|
||||||
{fulfillmentConsumables.length > 0 ? (
|
{fulfillmentConsumables.length > 0 ? (
|
||||||
@ -1243,7 +1250,9 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
{sellerConsumablesCost.toLocaleString('ru-RU')} ₽
|
{sellerConsumablesCost.toLocaleString('ru-RU')} ₽
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h6 className="text-blue-400 text-xs font-medium uppercase tracking-wider">🏪 Расходники сел.</h6>
|
<h6 className="text-blue-400 text-xs font-medium uppercase tracking-wider">
|
||||||
|
🏪 Расходники сел.
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
|
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
|
||||||
{sellerConsumables.length > 0 ? (
|
{sellerConsumables.length > 0 ? (
|
||||||
@ -1274,7 +1283,9 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="text-white/60 text-xs p-2 text-center bg-white/5 rounded-md">Загрузка...</div>
|
<div className="text-white/60 text-xs p-2 text-center bg-white/5 rounded-md">
|
||||||
|
Загрузка...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1300,9 +1311,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
<SelectValue placeholder="Не выбрано" />
|
<SelectValue placeholder="Не выбрано" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">
|
<SelectItem value="none">Не выбрано</SelectItem>
|
||||||
Не выбрано
|
|
||||||
</SelectItem>
|
|
||||||
{/* TODO: Загружать из БД */}
|
{/* TODO: Загружать из БД */}
|
||||||
<SelectItem value="card1">Карточка 1</SelectItem>
|
<SelectItem value="card1">Карточка 1</SelectItem>
|
||||||
<SelectItem value="card2">Карточка 2</SelectItem>
|
<SelectItem value="card2">Карточка 2</SelectItem>
|
||||||
@ -1341,7 +1350,7 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
{selectedGoods.map((item) => {
|
{selectedGoods.map((item) => {
|
||||||
// Используем единую функцию расчета
|
// Используем единую функцию расчета
|
||||||
const itemTotalPrice = getProductTotalWithRecipe(item.id, item.selectedQuantity)
|
const itemTotalPrice = getProductTotalWithRecipe(item.id, item.selectedQuantity)
|
||||||
const basePrice = item.price
|
const _basePrice = item.price
|
||||||
const priceWithRecipe = itemTotalPrice / item.selectedQuantity
|
const priceWithRecipe = itemTotalPrice / item.selectedQuantity
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1387,9 +1396,9 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<p className="text-white/60 text-xs">Фулфилмент-центр:</p>
|
<p className="text-white/60 text-xs">Фулфилмент-центр:</p>
|
||||||
<p className="text-white text-xs font-medium">
|
<p className="text-white text-xs font-medium">
|
||||||
{allCounterparties?.find(c => c.id === selectedFulfillment)?.name ||
|
{allCounterparties?.find((c) => c.id === selectedFulfillment)?.name ||
|
||||||
allCounterparties?.find(c => c.id === selectedFulfillment)?.fullName ||
|
allCounterparties?.find((c) => c.id === selectedFulfillment)?.fullName ||
|
||||||
'Выбранный центр'}
|
'Выбранный центр'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -1406,7 +1415,11 @@ export function CreateSuppliersSupplyPage() {
|
|||||||
</option>
|
</option>
|
||||||
{logisticsCompanies.length > 0 ? (
|
{logisticsCompanies.length > 0 ? (
|
||||||
logisticsCompanies.map((logisticsPartner) => (
|
logisticsCompanies.map((logisticsPartner) => (
|
||||||
<option key={logisticsPartner.id} value={logisticsPartner.id} className="bg-gray-800 text-white">
|
<option
|
||||||
|
key={logisticsPartner.id}
|
||||||
|
value={logisticsPartner.id}
|
||||||
|
className="bg-gray-800 text-white"
|
||||||
|
>
|
||||||
{logisticsPartner.name || logisticsPartner.fullName}
|
{logisticsPartner.name || logisticsPartner.fullName}
|
||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
|
Reference in New Issue
Block a user