diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 00b587e..8226cf7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,14 +29,14 @@ model User { } model Admin { - id String @id @default(cuid()) - username String @unique - password String // Хеш пароля - email String? @unique - isActive Boolean @default(true) + id String @id @default(cuid()) + username String @unique + password String // Хеш пароля + email String? @unique + isActive Boolean @default(true) lastLogin DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("admins") } @@ -104,14 +104,15 @@ model Organization { users User[] logistics Logistics[] supplyOrders SupplyOrder[] - partnerSupplyOrders SupplyOrder[] @relation("SupplyOrderPartner") - fulfillmentSupplyOrders SupplyOrder[] @relation("SupplyOrderFulfillmentCenter") - logisticsSupplyOrders SupplyOrder[] @relation("SupplyOrderLogistics") + partnerSupplyOrders SupplyOrder[] @relation("SupplyOrderPartner") + fulfillmentSupplyOrders SupplyOrder[] @relation("SupplyOrderFulfillmentCenter") + logisticsSupplyOrders SupplyOrder[] @relation("SupplyOrderLogistics") wildberriesSupplies WildberriesSupply[] - supplySuppliers SupplySupplier[] @relation("SupplySuppliers") - externalAds ExternalAd[] @relation("ExternalAds") - wbWarehouseCaches WBWarehouseCache[] @relation("WBWarehouseCaches") - sellerStatsCaches SellerStatsCache[] @relation("SellerStatsCaches") + supplySuppliers SupplySupplier[] @relation("SupplySuppliers") + externalAds ExternalAd[] @relation("ExternalAds") + wbWarehouseCaches WBWarehouseCache[] @relation("WBWarehouseCaches") + sellerStatsCaches SellerStatsCache[] @relation("SellerStatsCaches") + sellerSupplies Supply[] @relation("SellerSupplies") @@map("organizations") } @@ -198,24 +199,28 @@ model Service { } model Supply { - id String @id @default(cuid()) + id String @id @default(cuid()) name String description String? - price Decimal @db.Decimal(10, 2) - quantity Int @default(0) - unit String @default("шт") - category String @default("Расходники") - status String @default("planned") // planned, in-transit, delivered, in-stock - date DateTime @default(now()) - supplier String @default("Не указан") - minStock Int @default(0) - currentStock Int @default(0) - usedStock Int @default(0) // Количество использованных расходников + price Decimal @db.Decimal(10, 2) + quantity Int @default(0) + unit String @default("шт") + category String @default("Расходники") + status String @default("planned") // planned, in-transit, delivered, in-stock + date DateTime @default(now()) + supplier String @default("Не указан") + minStock Int @default(0) + currentStock Int @default(0) + usedStock Int @default(0) // Количество использованных расходников imageUrl String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + type SupplyType @default(FULFILLMENT_CONSUMABLES) // Тип расходников + sellerOwnerId String? // ID селлера-владельца (для расходников селлеров) + sellerOwner Organization? @relation("SellerSupplies", fields: [sellerOwnerId], references: [id], onDelete: SetNull) + shopLocation String? // Местоположение в магазине фулфилмента + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) @@map("supplies") } @@ -231,37 +236,37 @@ model Category { } model Product { - id String @id @default(cuid()) - name String - article String - description String? - price Decimal @db.Decimal(12, 2) - pricePerSet Decimal? @db.Decimal(12, 2) - quantity Int @default(0) - setQuantity Int? - ordered Int? - inTransit Int? - stock Int? - sold Int? - type ProductType @default(PRODUCT) - categoryId String? - brand String? - color String? - size String? - weight Decimal? @db.Decimal(8, 3) - dimensions String? - material String? - images Json @default("[]") - mainImage String? - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - organizationId String - cartItems CartItem[] - favorites Favorites[] + id String @id @default(cuid()) + name String + article String + description String? + price Decimal @db.Decimal(12, 2) + pricePerSet Decimal? @db.Decimal(12, 2) + quantity Int @default(0) + setQuantity Int? + ordered Int? + inTransit Int? + stock Int? + sold Int? + type ProductType @default(PRODUCT) + categoryId String? + brand String? + color String? + size String? + weight Decimal? @db.Decimal(8, 3) + dimensions String? + material String? + images Json @default("[]") + mainImage String? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String + cartItems CartItem[] + favorites Favorites[] supplyOrderItems SupplyOrderItem[] - category Category? @relation(fields: [categoryId], references: [id]) - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + category Category? @relation(fields: [categoryId], references: [id]) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) @@unique([organizationId, article]) @@map("products") @@ -355,41 +360,41 @@ model EmployeeSchedule { } model WildberriesSupply { - id String @id @default(cuid()) - organizationId String - deliveryDate DateTime? - status WildberriesSupplyStatus @default(DRAFT) - totalAmount Decimal @db.Decimal(12, 2) - totalItems Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - cards WildberriesSupplyCard[] + id String @id @default(cuid()) + organizationId String + deliveryDate DateTime? + status WildberriesSupplyStatus @default(DRAFT) + totalAmount Decimal @db.Decimal(12, 2) + totalItems Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + cards WildberriesSupplyCard[] @@map("wildberries_supplies") } model WildberriesSupplyCard { - id String @id @default(cuid()) - supplyId String - nmId String - vendorCode String - title String - brand String? - price Decimal @db.Decimal(12, 2) - discountedPrice Decimal? @db.Decimal(12, 2) - quantity Int - selectedQuantity Int - selectedMarket String? - selectedPlace String? - sellerName String? - sellerPhone String? - deliveryDate DateTime? - mediaFiles Json @default("[]") - selectedServices Json @default("[]") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - supply WildberriesSupply @relation(fields: [supplyId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + supplyId String + nmId String + vendorCode String + title String + brand String? + price Decimal @db.Decimal(12, 2) + discountedPrice Decimal? @db.Decimal(12, 2) + quantity Int + selectedQuantity Int + selectedMarket String? + selectedPlace String? + sellerName String? + sellerPhone String? + deliveryDate DateTime? + mediaFiles Json @default("[]") + selectedServices Json @default("[]") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + supply WildberriesSupply @relation(fields: [supplyId], references: [id], onDelete: Cascade) @@map("wildberries_supply_cards") } @@ -459,6 +464,11 @@ enum ProductType { CONSUMABLE } +enum SupplyType { + FULFILLMENT_CONSUMABLES // Расходники фулфилмента (купленные фулфилментом для себя) + SELLER_CONSUMABLES // Расходники селлеров (принятые от селлеров для хранения) +} + model Logistics { id String @id @default(cuid()) fromLocation String @@ -475,23 +485,23 @@ model Logistics { } model SupplyOrder { - id String @id @default(cuid()) - partnerId String - deliveryDate DateTime - status SupplyOrderStatus @default(PENDING) - totalAmount Decimal @db.Decimal(12, 2) - totalItems Int - fulfillmentCenterId String? - logisticsPartnerId String - consumableType String? // Классификация расходников: FULFILLMENT_CONSUMABLES, SELLER_CONSUMABLES - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - organizationId String - items SupplyOrderItem[] - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - partner Organization @relation("SupplyOrderPartner", fields: [partnerId], references: [id]) - fulfillmentCenter Organization? @relation("SupplyOrderFulfillmentCenter", fields: [fulfillmentCenterId], references: [id]) - logisticsPartner Organization @relation("SupplyOrderLogistics", fields: [logisticsPartnerId], references: [id]) + id String @id @default(cuid()) + partnerId String + deliveryDate DateTime + status SupplyOrderStatus @default(PENDING) + totalAmount Decimal @db.Decimal(12, 2) + totalItems Int + fulfillmentCenterId String? + logisticsPartnerId String? // Опциональная логистика - может назначить фулфилмент + consumableType String? // Классификация расходников: FULFILLMENT_CONSUMABLES, SELLER_CONSUMABLES + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String + items SupplyOrderItem[] + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + partner Organization @relation("SupplyOrderPartner", fields: [partnerId], references: [id]) + fulfillmentCenter Organization? @relation("SupplyOrderFulfillmentCenter", fields: [fulfillmentCenterId], references: [id]) + logisticsPartner Organization? @relation("SupplyOrderLogistics", fields: [logisticsPartnerId], references: [id]) @@map("supply_orders") } @@ -531,13 +541,13 @@ model SupplySupplier { model ExternalAd { id String @id @default(cuid()) - name String // Название рекламы - url String // URL рекламы + name String // Название рекламы + url String // URL рекламы cost Decimal @db.Decimal(12, 2) // Стоимость - date DateTime // Дата рекламы - nmId String // ID товара Wildberries + date DateTime // Дата рекламы + nmId String // ID товара Wildberries clicks Int @default(0) // Количество кликов - organizationId String // ID организации + organizationId String // ID организации createdAt DateTime @default(now()) updatedAt DateTime @updatedAt organization Organization @relation("ExternalAds", fields: [organizationId], references: [id], onDelete: Cascade) @@ -548,9 +558,9 @@ model ExternalAd { model WBWarehouseCache { id String @id @default(cuid()) - organizationId String // ID организации - cacheDate DateTime // Дата кеширования (только дата, без времени) - data Json // Кешированные данные склада WB + organizationId String // ID организации + cacheDate DateTime // Дата кеширования (только дата, без времени) + data Json // Кешированные данные склада WB totalProducts Int @default(0) // Общее количество товаров totalStocks Int @default(0) // Общее количество остатков totalReserved Int @default(0) // Общее количество в резерве @@ -564,30 +574,30 @@ model WBWarehouseCache { } model SellerStatsCache { - id String @id @default(cuid()) - organizationId String // ID организации - cacheDate DateTime // Дата кеширования (только дата, без времени) - period String // Период статистики (week, month, quarter, custom) - dateFrom DateTime? // Дата начала периода (для custom) - dateTo DateTime? // Дата окончания периода (для custom) - + id String @id @default(cuid()) + organizationId String // ID организации + cacheDate DateTime // Дата кеширования (только дата, без времени) + period String // Период статистики (week, month, quarter, custom) + dateFrom DateTime? // Дата начала периода (для custom) + dateTo DateTime? // Дата окончания периода (для custom) + // Данные товаров - productsData Json? // Кешированные данные товаров - productsTotalSales Decimal? @db.Decimal(15, 2) // Общая сумма продаж товаров - productsTotalOrders Int? // Общее количество заказов товаров - productsCount Int? // Количество товаров - + productsData Json? // Кешированные данные товаров + productsTotalSales Decimal? @db.Decimal(15, 2) // Общая сумма продаж товаров + productsTotalOrders Int? // Общее количество заказов товаров + productsCount Int? // Количество товаров + // Данные рекламы - advertisingData Json? // Кешированные данные рекламы - advertisingTotalCost Decimal? @db.Decimal(15, 2) // Общие расходы на рекламу - advertisingTotalViews Int? // Общие показы рекламы - advertisingTotalClicks Int? // Общие клики рекламы - + advertisingData Json? // Кешированные данные рекламы + advertisingTotalCost Decimal? @db.Decimal(15, 2) // Общие расходы на рекламу + advertisingTotalViews Int? // Общие показы рекламы + advertisingTotalClicks Int? // Общие клики рекламы + // Метаданные - expiresAt DateTime // Время истечения кеша (24 часа) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - organization Organization @relation("SellerStatsCaches", fields: [organizationId], references: [id], onDelete: Cascade) + expiresAt DateTime // Время истечения кеша (24 часа) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organization Organization @relation("SellerStatsCaches", fields: [organizationId], references: [id], onDelete: Cascade) @@unique([organizationId, cacheDate, period, dateFrom, dateTo]) @@index([organizationId, cacheDate]) diff --git a/src/app/api/graphql/route.ts b/src/app/api/graphql/route.ts index d05d64f..62e474f 100644 --- a/src/app/api/graphql/route.ts +++ b/src/app/api/graphql/route.ts @@ -1,84 +1,82 @@ -import { ApolloServer } from '@apollo/server' -import { startServerAndCreateNextHandler } from '@as-integrations/next' -import { NextRequest } from 'next/server' -import jwt from 'jsonwebtoken' -import { typeDefs } from '@/graphql/typedefs' -import { resolvers } from '@/graphql/resolvers' - -// Интерфейс для контекста -interface Context { - user?: { - id: string - phone: string - } - admin?: { - id: string - username: string - } -} +import { ApolloServer } from "@apollo/server"; +import { startServerAndCreateNextHandler } from "@as-integrations/next"; +import { NextRequest } from "next/server"; +import jwt from "jsonwebtoken"; +import { typeDefs } from "@/graphql/typedefs"; +import { resolvers } from "@/graphql/resolvers"; +import { Context } from "@/graphql/context"; // Создаем Apollo Server const server = new ApolloServer({ typeDefs, resolvers, -}) +}); // Создаем Next.js handler const handler = startServerAndCreateNextHandler(server, { context: async (req: NextRequest) => { // Извлекаем токен из заголовка Authorization - const authHeader = req.headers.get('authorization') - const token = authHeader?.replace('Bearer ', '') + const authHeader = req.headers.get("authorization"); + const token = authHeader?.replace("Bearer ", ""); - console.log('GraphQL Context - Auth header:', authHeader) - console.log('GraphQL Context - Token:', token ? `${token.substring(0, 20)}...` : 'No token') + console.log("GraphQL Context - Auth header:", authHeader); + console.log( + "GraphQL Context - Token:", + token ? `${token.substring(0, 20)}...` : "No token" + ); if (!token) { - console.log('GraphQL Context - No token provided') - return { user: undefined, admin: undefined } + console.log("GraphQL Context - No token provided"); + return { user: undefined, admin: undefined }; } try { // Верифицируем JWT токен const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { - userId?: string - phone?: string - adminId?: string - username?: string - type?: string - } + userId?: string; + phone?: string; + adminId?: string; + username?: string; + type?: string; + }; // Проверяем тип токена - if (decoded.type === 'admin' && decoded.adminId && decoded.username) { - console.log('GraphQL Context - Decoded admin:', { id: decoded.adminId, username: decoded.username }) + if (decoded.type === "admin" && decoded.adminId && decoded.username) { + console.log("GraphQL Context - Decoded admin:", { + id: decoded.adminId, + username: decoded.username, + }); return { admin: { id: decoded.adminId, - username: decoded.username - } - } + username: decoded.username, + }, + }; } else if (decoded.userId && decoded.phone) { - console.log('GraphQL Context - Decoded user:', { id: decoded.userId, phone: decoded.phone }) + console.log("GraphQL Context - Decoded user:", { + id: decoded.userId, + phone: decoded.phone, + }); return { user: { id: decoded.userId, - phone: decoded.phone - } - } + phone: decoded.phone, + }, + }; } - return { user: undefined, admin: undefined } + return { user: undefined, admin: undefined }; } catch (error) { - console.error('GraphQL Context - Invalid token:', error) - return { user: undefined, admin: undefined } + console.error("GraphQL Context - Invalid token:", error); + return { user: undefined, admin: undefined }; } - } -}) + }, +}); export async function GET(request: NextRequest) { - return handler(request) + return handler(request); } export async function POST(request: NextRequest) { - return handler(request) -} \ No newline at end of file + return handler(request); +} diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index 5f0e2cb..581b3ab 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -48,6 +48,65 @@ function PendingSuppliesNotification() { ); } +// Компонент для отображения логистических заявок (только для логистики) +function LogisticsOrdersNotification() { + const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { + pollInterval: 30000, // Обновляем каждые 30 секунд + fetchPolicy: "cache-first", + errorPolicy: "ignore", + }); + + const logisticsCount = + pendingData?.pendingSuppliesCount?.logisticsOrders || 0; + + if (logisticsCount === 0) return null; + + return ( +
+ {logisticsCount > 99 ? "99+" : logisticsCount} +
+ ); +} + +// Компонент для отображения поставок фулфилмента (только поставки, не заявки на партнерство) +function FulfillmentSuppliesNotification() { + const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { + pollInterval: 30000, // Обновляем каждые 30 секунд + fetchPolicy: "cache-first", + errorPolicy: "ignore", + }); + + const suppliesCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0; + + if (suppliesCount === 0) return null; + + return ( +
+ {suppliesCount > 99 ? "99+" : suppliesCount} +
+ ); +} + +// Компонент для отображения входящих заказов поставщика (только входящие заказы, не заявки на партнерство) +function WholesaleOrdersNotification() { + const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { + pollInterval: 30000, // Обновляем каждые 30 секунд + fetchPolicy: "cache-first", + errorPolicy: "ignore", + }); + + const ordersCount = + pendingData?.pendingSuppliesCount?.incomingSupplierOrders || 0; + + if (ordersCount === 0) return null; + + return ( +
+ {ordersCount > 99 ? "99+" : ordersCount} +
+ ); +} + export function Sidebar() { const { user, logout } = useAuth(); const router = useRouter(); @@ -149,7 +208,7 @@ export function Sidebar() { router.push("/supplies"); break; case "WHOLESALE": - router.push("/supplies"); + router.push("/supplier-orders"); break; case "LOGIST": router.push("/logistics-orders"); @@ -202,7 +261,8 @@ export function Sidebar() { const isSuppliesActive = pathname.startsWith("/supplies") || pathname.startsWith("/fulfillment-supplies") || - pathname.startsWith("/logistics"); + pathname.startsWith("/logistics") || + pathname.startsWith("/supplier-orders"); const isPartnersActive = pathname.startsWith("/partners"); return ( @@ -475,8 +535,7 @@ export function Sidebar() { > {!isCollapsed && Мои поставки} - {/* Уведомление о непринятых поставках */} - + {/* Селлеры не получают уведомления о поставках - только отслеживают статус */} )} @@ -536,8 +595,8 @@ export function Sidebar() { {!isCollapsed && ( Входящие поставки )} - {/* Уведомление о непринятых поставках */} - + {/* Уведомление только о поставках, не о заявках на партнерство */} + )} @@ -595,8 +654,8 @@ export function Sidebar() { > {!isCollapsed && Заявки} - {/* Уведомление о непринятых поставках */} - + {/* Уведомление только о входящих заказах поставок, не о заявках на партнерство */} + )} @@ -616,8 +675,8 @@ export function Sidebar() { > {!isCollapsed && Перевозки} - {/* Уведомление о непринятых поставках */} - + {/* Уведомление только о логистических заявках */} + )} diff --git a/src/components/employees/employees-dashboard.tsx b/src/components/employees/employees-dashboard.tsx index 125d103..e14538a 100644 --- a/src/components/employees/employees-dashboard.tsx +++ b/src/components/employees/employees-dashboard.tsx @@ -1,86 +1,104 @@ -"use client" +"use client"; -import { useState, useEffect, useMemo } from 'react' -import { useQuery, useMutation } from '@apollo/client' -import { apolloClient } from '@/lib/apollo-client' -import { Sidebar } from '@/components/dashboard/sidebar' -import { Card } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { useState, useEffect, useMemo } from "react"; +import { useQuery, useMutation } from "@apollo/client"; +import { apolloClient } from "@/lib/apollo-client"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { EmployeeInlineForm } from './employee-inline-form' -import { EmployeeCompactForm } from './employee-compact-form' -import { EmployeeEditInlineForm } from './employee-edit-inline-form' +import { EmployeeInlineForm } from "./employee-inline-form"; +import { EmployeeCompactForm } from "./employee-compact-form"; +import { EmployeeEditInlineForm } from "./employee-edit-inline-form"; -import { EmployeeSearch } from './employee-search' -import { EmployeeLegend } from './employee-legend' -import { EmployeeEmptyState } from './employee-empty-state' -import { EmployeeRow } from './employee-row' -import { EmployeeReports } from './employee-reports' -import { toast } from 'sonner' -import { GET_MY_EMPLOYEES, GET_EMPLOYEE_SCHEDULE } from '@/graphql/queries' -import { CREATE_EMPLOYEE, UPDATE_EMPLOYEE, DELETE_EMPLOYEE, UPDATE_EMPLOYEE_SCHEDULE } from '@/graphql/mutations' -import { Users, FileText, Plus, Layout, LayoutGrid } from 'lucide-react' +import { EmployeeSearch } from "./employee-search"; +import { EmployeeLegend } from "./employee-legend"; +import { EmployeeEmptyState } from "./employee-empty-state"; +import { EmployeeRow } from "./employee-row"; +import { EmployeeReports } from "./employee-reports"; +import { toast } from "sonner"; +import { GET_MY_EMPLOYEES, GET_EMPLOYEE_SCHEDULE } from "@/graphql/queries"; +import { + CREATE_EMPLOYEE, + UPDATE_EMPLOYEE, + DELETE_EMPLOYEE, + UPDATE_EMPLOYEE_SCHEDULE, +} from "@/graphql/mutations"; +import { Users, FileText, Plus, Layout, LayoutGrid } from "lucide-react"; // Интерфейс сотрудника interface Employee { - id: string - firstName: string - lastName: string - middleName?: string - position: string - phone: string - email?: string - avatar?: string - hireDate: string - status: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED' - salary?: number - address?: string - birthDate?: string - passportSeries?: string - passportNumber?: string - passportIssued?: string - passportDate?: string - emergencyContact?: string - emergencyPhone?: string - telegram?: string - whatsapp?: string - passportPhoto?: string - createdAt: string - updatedAt: string + id: string; + firstName: string; + lastName: string; + middleName?: string; + position: string; + phone: string; + email?: string; + avatar?: string; + hireDate: string; + status: "ACTIVE" | "VACATION" | "SICK" | "FIRED"; + salary?: number; + address?: string; + birthDate?: string; + passportSeries?: string; + passportNumber?: string; + passportIssued?: string; + passportDate?: string; + emergencyContact?: string; + emergencyPhone?: string; + telegram?: string; + whatsapp?: string; + passportPhoto?: string; + createdAt: string; + updatedAt: string; } export function EmployeesDashboard() { - const [searchQuery, setSearchQuery] = useState('') - const [showAddForm, setShowAddForm] = useState(false) - const [showCompactForm, setShowCompactForm] = useState(true) // По умолчанию компактная форма - const [showEditForm, setShowEditForm] = useState(false) - const [createLoading, setCreateLoading] = useState(false) - const [editingEmployee, setEditingEmployee] = useState(null) - const [deletingEmployeeId, setDeletingEmployeeId] = useState(null) - const [employeeSchedules, setEmployeeSchedules] = useState<{[key: string]: ScheduleRecord[]}>({}) - const [currentYear, setCurrentYear] = useState(new Date().getFullYear()) - const [currentMonth, setCurrentMonth] = useState(new Date().getMonth()) - const [activeTab, setActiveTab] = useState('combined') + const [searchQuery, setSearchQuery] = useState(""); + const [showAddForm, setShowAddForm] = useState(false); + const [showCompactForm, setShowCompactForm] = useState(true); // По умолчанию компактная форма + const [showEditForm, setShowEditForm] = useState(false); + const [createLoading, setCreateLoading] = useState(false); + const [editingEmployee, setEditingEmployee] = useState(null); + const [deletingEmployeeId, setDeletingEmployeeId] = useState( + null + ); + const [employeeSchedules, setEmployeeSchedules] = useState<{ + [key: string]: ScheduleRecord[]; + }>({}); + const [currentYear, setCurrentYear] = useState(new Date().getFullYear()); + const [currentMonth, setCurrentMonth] = useState(new Date().getMonth()); + const [activeTab, setActiveTab] = useState("combined"); interface ScheduleRecord { - id: string - date: string - status: string - hoursWorked?: number + id: string; + date: string; + status: string; + hoursWorked?: number; employee: { - id: string - } + id: string; + }; } - + // GraphQL запросы и мутации - const { data, loading, refetch } = useQuery(GET_MY_EMPLOYEES) - const [createEmployee] = useMutation(CREATE_EMPLOYEE) - const [updateEmployee] = useMutation(UPDATE_EMPLOYEE) - const [deleteEmployee] = useMutation(DELETE_EMPLOYEE) - const [updateEmployeeSchedule] = useMutation(UPDATE_EMPLOYEE_SCHEDULE) - - const employees = useMemo(() => data?.myEmployees || [], [data?.myEmployees]) + const { data, loading, refetch } = useQuery(GET_MY_EMPLOYEES); + const [createEmployee] = useMutation(CREATE_EMPLOYEE, { + refetchQueries: [{ query: GET_MY_EMPLOYEES }], + onCompleted: () => { + refetch(); // Принудительно обновляем список + }, + }); + const [updateEmployee] = useMutation(UPDATE_EMPLOYEE, { + refetchQueries: [{ query: GET_MY_EMPLOYEES }], + }); + const [deleteEmployee] = useMutation(DELETE_EMPLOYEE, { + refetchQueries: [{ query: GET_MY_EMPLOYEES }], + }); + const [updateEmployeeSchedule] = useMutation(UPDATE_EMPLOYEE_SCHEDULE); + + const employees = useMemo(() => data?.myEmployees || [], [data?.myEmployees]); // Загружаем данные табеля для всех сотрудников useEffect(() => { @@ -93,202 +111,217 @@ export function EmployeesDashboard() { variables: { employeeId: employee.id, year: currentYear, - month: currentMonth - } - }) - return { employeeId: employee.id, scheduleData: data?.employeeSchedule || [] } + month: currentMonth, + }, + }); + return { + employeeId: employee.id, + scheduleData: data?.employeeSchedule || [], + }; } catch (error) { - console.error(`Error loading schedule for ${employee.id}:`, error) - return { employeeId: employee.id, scheduleData: [] } + console.error(`Error loading schedule for ${employee.id}:`, error); + return { employeeId: employee.id, scheduleData: [] }; } - }) + }); - const results = await Promise.all(schedulePromises) - const scheduleMap: {[key: string]: ScheduleRecord[]} = {} - results.forEach((result: { employeeId: string; scheduleData: ScheduleRecord[] }) => { - if (result && result.scheduleData) { - scheduleMap[result.employeeId] = result.scheduleData + const results = await Promise.all(schedulePromises); + const scheduleMap: { [key: string]: ScheduleRecord[] } = {}; + results.forEach( + (result: { employeeId: string; scheduleData: ScheduleRecord[] }) => { + if (result && result.scheduleData) { + scheduleMap[result.employeeId] = result.scheduleData; + } } - }) - setEmployeeSchedules(scheduleMap) + ); + setEmployeeSchedules(scheduleMap); } - } + }; - loadScheduleData() - }, [employees, currentYear, currentMonth]) + loadScheduleData(); + }, [employees, currentYear, currentMonth]); const handleEditEmployee = (employee: Employee) => { - setEditingEmployee(employee) - setShowEditForm(true) - setShowAddForm(false) // Закрываем форму добавления если открыта - } + setEditingEmployee(employee); + setShowEditForm(true); + setShowAddForm(false); // Закрываем форму добавления если открыта + }; const handleEmployeeSaved = async (employeeData: Partial) => { try { if (editingEmployee) { // Обновление существующего сотрудника const { data } = await updateEmployee({ - variables: { + variables: { id: editingEmployee.id, - input: employeeData - } - }) + input: employeeData, + }, + }); if (data?.updateEmployee?.success) { - toast.success('Сотрудник успешно обновлен') - refetch() + toast.success("Сотрудник успешно обновлен"); + refetch(); } } else { - // Добавление нового сотрудника + // Добавление нового сотрудника const { data } = await createEmployee({ - variables: { input: employeeData } - }) + variables: { input: employeeData }, + }); if (data?.createEmployee?.success) { - toast.success('Сотрудник успешно добавлен') - refetch() + toast.success("Сотрудник успешно добавлен"); + refetch(); } } - setShowEditForm(false) - setEditingEmployee(null) + setShowEditForm(false); + setEditingEmployee(null); } catch (error) { - console.error('Error saving employee:', error) - toast.error('Ошибка при сохранении сотрудника') + console.error("Error saving employee:", error); + toast.error("Ошибка при сохранении сотрудника"); } - } + }; const handleCreateEmployee = async (employeeData: Partial) => { - setCreateLoading(true) + setCreateLoading(true); try { const { data } = await createEmployee({ - variables: { input: employeeData } - }) + variables: { input: employeeData }, + }); if (data?.createEmployee?.success) { - toast.success('Сотрудник успешно добавлен!') - setShowAddForm(false) - refetch() + toast.success("Сотрудник успешно добавлен!"); + setShowAddForm(false); + refetch(); } } catch (error) { - console.error('Error creating employee:', error) - toast.error('Ошибка при создании сотрудника') + console.error("Error creating employee:", error); + toast.error("Ошибка при создании сотрудника"); } finally { - setCreateLoading(false) + setCreateLoading(false); } - } + }; const handleEmployeeDeleted = async (employeeId: string) => { try { - setDeletingEmployeeId(employeeId) + setDeletingEmployeeId(employeeId); const { data } = await deleteEmployee({ - variables: { id: employeeId } - }) + variables: { id: employeeId }, + }); if (data?.deleteEmployee) { - toast.success('Сотрудник успешно уволен') - refetch() + toast.success("Сотрудник успешно уволен"); + refetch(); } } catch (error) { - console.error('Error deleting employee:', error) - toast.error('Ошибка при увольнении сотрудника') + console.error("Error deleting employee:", error); + toast.error("Ошибка при увольнении сотрудника"); } finally { - setDeletingEmployeeId(null) + setDeletingEmployeeId(null); } - } + }; // Функция для изменения статуса дня в табеле - const changeDayStatus = async (employeeId: string, day: number, currentStatus: string) => { + const changeDayStatus = async ( + employeeId: string, + day: number, + currentStatus: string + ) => { try { // Циклично переключаем статусы - const statuses = ['WORK', 'WEEKEND', 'VACATION', 'SICK', 'ABSENT'] - const currentIndex = statuses.indexOf(currentStatus.toUpperCase()) - const nextStatus = statuses[(currentIndex + 1) % statuses.length] - + const statuses = ["WORK", "WEEKEND", "VACATION", "SICK", "ABSENT"]; + const currentIndex = statuses.indexOf(currentStatus.toUpperCase()); + const nextStatus = statuses[(currentIndex + 1) % statuses.length]; + // Формируем дату - const date = new Date(currentYear, currentMonth, day) - const hours = nextStatus === 'WORK' ? 8 : 0 - + const date = new Date(currentYear, currentMonth, day); + const hours = nextStatus === "WORK" ? 8 : 0; + // Отправляем мутацию await updateEmployeeSchedule({ variables: { input: { employeeId: employeeId, - date: date.toISOString().split('T')[0], // YYYY-MM-DD формат + date: date.toISOString().split("T")[0], // YYYY-MM-DD формат status: nextStatus, - hoursWorked: hours - } - } - }) + hoursWorked: hours, + }, + }, + }); - // Обновляем локальное состояние - const updatedDate = new Date(currentYear, currentMonth, day) - const dateStr = updatedDate.toISOString().split('T')[0] - - setEmployeeSchedules(prev => { - const currentSchedule = prev[employeeId] || [] - const existingRecordIndex = currentSchedule.findIndex(record => - record.date.split('T')[0] === dateStr - ) - - const newRecord = { - id: Date.now().toString(), // временный ID - date: updatedDate.toISOString(), - status: nextStatus, - hoursWorked: hours, - employee: { id: employeeId } - } - - let updatedSchedule + // Обновляем локальное состояние + const updatedDate = new Date(currentYear, currentMonth, day); + const dateStr = updatedDate.toISOString().split("T")[0]; + + setEmployeeSchedules((prev) => { + const currentSchedule = prev[employeeId] || []; + const existingRecordIndex = currentSchedule.findIndex( + (record) => record.date.split("T")[0] === dateStr + ); + + const newRecord = { + id: Date.now().toString(), // временный ID + date: updatedDate.toISOString(), + status: nextStatus, + hoursWorked: hours, + employee: { id: employeeId }, + }; + + let updatedSchedule; if (existingRecordIndex >= 0) { // Обновляем существующую запись - updatedSchedule = [...currentSchedule] - updatedSchedule[existingRecordIndex] = { ...updatedSchedule[existingRecordIndex], ...newRecord } + updatedSchedule = [...currentSchedule]; + updatedSchedule[existingRecordIndex] = { + ...updatedSchedule[existingRecordIndex], + ...newRecord, + }; } else { // Добавляем новую запись - updatedSchedule = [...currentSchedule, newRecord] + updatedSchedule = [...currentSchedule, newRecord]; } - + return { ...prev, - [employeeId]: updatedSchedule - } - }) - - toast.success('Статус дня обновлен') - + [employeeId]: updatedSchedule, + }; + }); + + toast.success("Статус дня обновлен"); } catch (error) { - console.error('Error updating day status:', error) - toast.error('Ошибка при обновлении статуса дня') + console.error("Error updating day status:", error); + toast.error("Ошибка при обновлении статуса дня"); } - } + }; // Функция для обновления данных дня из модалки - const updateDayData = async (employeeId: string, date: Date, data: { - status: string - hoursWorked?: number - overtimeHours?: number - notes?: string - }) => { + const updateDayData = async ( + employeeId: string, + date: Date, + data: { + status: string; + hoursWorked?: number; + overtimeHours?: number; + notes?: string; + } + ) => { try { // Отправляем мутацию await updateEmployeeSchedule({ variables: { input: { employeeId: employeeId, - date: date.toISOString().split('T')[0], // YYYY-MM-DD формат + date: date.toISOString().split("T")[0], // YYYY-MM-DD формат status: data.status, hoursWorked: data.hoursWorked, overtimeHours: data.overtimeHours, - notes: data.notes - } - } - }) + notes: data.notes, + }, + }, + }); // Обновляем локальное состояние - const dateStr = date.toISOString().split('T')[0] - - setEmployeeSchedules(prev => { - const currentSchedule = prev[employeeId] || [] - const existingRecordIndex = currentSchedule.findIndex(record => - record.date.split('T')[0] === dateStr - ) - + const dateStr = date.toISOString().split("T")[0]; + + setEmployeeSchedules((prev) => { + const currentSchedule = prev[employeeId] || []; + const existingRecordIndex = currentSchedule.findIndex( + (record) => record.date.split("T")[0] === dateStr + ); + const newRecord = { id: Date.now().toString(), // временный ID date: date.toISOString(), @@ -296,73 +329,98 @@ export function EmployeesDashboard() { hoursWorked: data.hoursWorked, overtimeHours: data.overtimeHours, notes: data.notes, - employee: { id: employeeId } - } - - let updatedSchedule + employee: { id: employeeId }, + }; + + let updatedSchedule; if (existingRecordIndex >= 0) { // Обновляем существующую запись - updatedSchedule = [...currentSchedule] - updatedSchedule[existingRecordIndex] = { ...updatedSchedule[existingRecordIndex], ...newRecord } + updatedSchedule = [...currentSchedule]; + updatedSchedule[existingRecordIndex] = { + ...updatedSchedule[existingRecordIndex], + ...newRecord, + }; } else { // Добавляем новую запись - updatedSchedule = [...currentSchedule, newRecord] + updatedSchedule = [...currentSchedule, newRecord]; } - + return { ...prev, - [employeeId]: updatedSchedule - } - }) - - toast.success('Данные дня обновлены') - + [employeeId]: updatedSchedule, + }; + }); + + toast.success("Данные дня обновлены"); } catch (error) { - console.error('Error updating day data:', error) - toast.error('Ошибка при обновлении данных дня') + console.error("Error updating day data:", error); + toast.error("Ошибка при обновлении данных дня"); } - } + }; const exportToCSV = () => { const csvContent = [ - ['ФИО', 'Должность', 'Статус', 'Зарплата', 'Телефон', 'Email', 'Дата найма'], + [ + "ФИО", + "Должность", + "Статус", + "Зарплата", + "Телефон", + "Email", + "Дата найма", + ], ...employees.map((emp: Employee) => [ `${emp.firstName} ${emp.lastName}`, emp.position, - emp.status === 'ACTIVE' ? 'Активен' : - emp.status === 'VACATION' ? 'В отпуске' : - emp.status === 'SICK' ? 'На больничном' : 'Уволен', - emp.salary?.toString() || '', + emp.status === "ACTIVE" + ? "Активен" + : emp.status === "VACATION" + ? "В отпуске" + : emp.status === "SICK" + ? "На больничном" + : "Уволен", + emp.salary?.toString() || "", emp.phone, - emp.email || '', - new Date(emp.hireDate).toLocaleDateString('ru-RU') - ]) - ].map(row => row.join(',')).join('\n') + emp.email || "", + new Date(emp.hireDate).toLocaleDateString("ru-RU"), + ]), + ] + .map((row) => row.join(",")) + .join("\n"); - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) - const link = document.createElement('a') - const url = URL.createObjectURL(blob) - link.setAttribute('href', url) - link.setAttribute('download', `employees_report_${new Date().toISOString().split('T')[0]}.csv`) - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - toast.success('Отчет успешно экспортирован') - } + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute( + "download", + `employees_report_${new Date().toISOString().split("T")[0]}.csv` + ); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + toast.success("Отчет успешно экспортирован"); + }; const generateReport = () => { const stats = { total: employees.length, - active: employees.filter((e: Employee) => e.status === 'ACTIVE').length, - vacation: employees.filter((e: Employee) => e.status === 'VACATION').length, - sick: employees.filter((e: Employee) => e.status === 'SICK').length, - inactive: employees.filter((e: Employee) => e.status === 'FIRED').length, - avgSalary: Math.round(employees.reduce((sum: number, e: Employee) => sum + (e.salary || 0), 0) / employees.length) - } + active: employees.filter((e: Employee) => e.status === "ACTIVE").length, + vacation: employees.filter((e: Employee) => e.status === "VACATION") + .length, + sick: employees.filter((e: Employee) => e.status === "SICK").length, + inactive: employees.filter((e: Employee) => e.status === "FIRED").length, + avgSalary: Math.round( + employees.reduce( + (sum: number, e: Employee) => sum + (e.salary || 0), + 0 + ) / employees.length + ), + }; const reportText = ` ОТЧЕТ ПО СОТРУДНИКАМ -Дата: ${new Date().toLocaleDateString('ru-RU')} +Дата: ${new Date().toLocaleDateString("ru-RU")} ОБЩАЯ СТАТИСТИКА: • Всего сотрудников: ${stats.total} @@ -370,24 +428,29 @@ export function EmployeesDashboard() { • В отпуске: ${stats.vacation} • На больничном: ${stats.sick} • Неактивных: ${stats.inactive} -• Средняя зарплата: ${stats.avgSalary.toLocaleString('ru-RU')} ₽ +• Средняя зарплата: ${stats.avgSalary.toLocaleString("ru-RU")} ₽ СПИСОК СОТРУДНИКОВ: -${employees.map((emp: Employee) => - `• ${emp.firstName} ${emp.lastName} - ${emp.position}` -).join('\n')} - `.trim() +${employees + .map( + (emp: Employee) => `• ${emp.firstName} ${emp.lastName} - ${emp.position}` + ) + .join("\n")} + `.trim(); - const blob = new Blob([reportText], { type: 'text/plain;charset=utf-8;' }) - const link = document.createElement('a') - const url = URL.createObjectURL(blob) - link.setAttribute('href', url) - link.setAttribute('download', `employees_summary_${new Date().toISOString().split('T')[0]}.txt`) - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - toast.success('Сводный отчет создан') - } + const blob = new Blob([reportText], { type: "text/plain;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute( + "download", + `employees_summary_${new Date().toISOString().split("T")[0]}.txt` + ); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + toast.success("Сводный отчет создан"); + }; if (loading) { return ( @@ -401,7 +464,7 @@ ${employees.map((emp: Employee) => - ) + ); } return ( @@ -410,19 +473,23 @@ ${employees.map((emp: Employee) =>
{/* Панель управления с улучшенным расположением */} - +
{/* Красивые табы слева */} - Сотрудники - @@ -461,147 +528,166 @@ ${employees.map((emp: Employee) => )} )} - -
- {/* Форма добавления сотрудника */} - {showAddForm && ( - showCompactForm ? ( - setShowAddForm(false)} + {/* Форма добавления сотрудника */} + {showAddForm && + (showCompactForm ? ( + setShowAddForm(false)} + isLoading={createLoading} + /> + ) : ( + setShowAddForm(false)} + isLoading={createLoading} + /> + ))} + + {/* Форма редактирования сотрудника */} + {showEditForm && editingEmployee && ( + { + setShowEditForm(false); + setEditingEmployee(null); + }} isLoading={createLoading} /> - ) : ( - setShowAddForm(false)} - isLoading={createLoading} - /> - ) - )} + )} - {/* Форма редактирования сотрудника */} - {showEditForm && editingEmployee && ( - { - setShowEditForm(false) - setEditingEmployee(null) - }} - isLoading={createLoading} - /> - )} + {/* Контент табов */} + + + {(() => { + const filteredEmployees = employees.filter( + (employee: Employee) => + `${employee.firstName} ${employee.lastName}` + .toLowerCase() + .includes(searchQuery.toLowerCase()) || + employee.position + .toLowerCase() + .includes(searchQuery.toLowerCase()) + ); - {/* Контент табов */} - - - {(() => { - const filteredEmployees = employees.filter((employee: Employee) => - `${employee.firstName} ${employee.lastName}`.toLowerCase().includes(searchQuery.toLowerCase()) || - employee.position.toLowerCase().includes(searchQuery.toLowerCase()) - ) + if (filteredEmployees.length === 0) { + return ( + setShowAddForm(true)} + /> + ); + } - if (filteredEmployees.length === 0) { return ( - setShowAddForm(true)} - /> - ) - } +
+ {/* Навигация по месяцам и легенда в одной строке */} +
+
+

+ {new Date().toLocaleDateString("ru-RU", { + weekday: "long", + day: "numeric", + month: "long", + year: "numeric", + })} +

- return ( -
- {/* Навигация по месяцам и легенда в одной строке */} -
-
-

- {new Date().toLocaleDateString('ru-RU', { - weekday: 'long', - day: 'numeric', - month: 'long', - year: 'numeric' - })} -

- - {/* Кнопки навигации */} -
- - - + {/* Кнопки навигации */} +
+ + + +
+ + {/* Легенда статусов справа */} +
- - {/* Легенда статусов справа */} - -
- {/* Компактный список сотрудников с раскрывающимся табелем */} -
- {filteredEmployees.map((employee: Employee, index: number) => ( -
- -
- ))} + {/* Компактный список сотрудников с раскрывающимся табелем */} +
+ {filteredEmployees.map( + (employee: Employee, index: number) => ( +
+ +
+ ) + )} +
-
- ) - })()} - - + ); + })()} + +
- ) -} \ No newline at end of file + ); +} diff --git a/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx b/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx index c9bd5ad..0a90599 100644 --- a/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx +++ b/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx @@ -99,14 +99,44 @@ export function CreateFulfillmentConsumablesSupplyPage() { GET_MY_COUNTERPARTIES ); + // ОТЛАДКА: Логируем состояние перед запросом товаров + console.log("🔍 ДИАГНОСТИКА ЗАПРОСА ТОВАРОВ:", { + selectedSupplier: selectedSupplier + ? { + id: selectedSupplier.id, + name: selectedSupplier.name || selectedSupplier.fullName, + type: selectedSupplier.type, + } + : null, + skipQuery: !selectedSupplier, + productSearchQuery, + }); + // Загружаем товары для выбранного поставщика - const { data: productsData, loading: productsLoading } = useQuery( - GET_ALL_PRODUCTS, - { - skip: !selectedSupplier, - variables: { search: productSearchQuery || null, category: null }, - } - ); + const { + data: productsData, + loading: productsLoading, + error: productsError, + } = useQuery(GET_ALL_PRODUCTS, { + skip: !selectedSupplier, + variables: { search: productSearchQuery || null, category: null }, + onCompleted: (data) => { + console.log("✅ GET_ALL_PRODUCTS COMPLETED:", { + totalProducts: data?.allProducts?.length || 0, + products: + data?.allProducts?.map((p) => ({ + id: p.id, + name: p.name, + type: p.type, + orgId: p.organization?.id, + orgName: p.organization?.name, + })) || [], + }); + }, + onError: (error) => { + console.error("❌ GET_ALL_PRODUCTS ERROR:", error); + }, + }); // Мутация для создания заказа поставки расходников const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER); @@ -117,9 +147,9 @@ export function CreateFulfillmentConsumablesSupplyPage() { ).filter((org: FulfillmentConsumableSupplier) => org.type === "WHOLESALE"); // Фильтруем только логистические компании - const logisticsPartners = ( - counterpartiesData?.myCounterparties || [] - ).filter((org: FulfillmentConsumableSupplier) => org.type === "LOGIST"); + const logisticsPartners = (counterpartiesData?.myCounterparties || []).filter( + (org: FulfillmentConsumableSupplier) => org.type === "LOGIST" + ); // Фильтруем поставщиков по поисковому запросу const filteredSuppliers = consumableSuppliers.filter( @@ -150,6 +180,7 @@ export function CreateFulfillmentConsumablesSupplyPage() { } : null, productsLoading, + productsError: productsError?.message, allProductsCount: productsData?.allProducts?.length || 0, supplierProductsCount: supplierProducts.length, allProducts: @@ -160,14 +191,20 @@ export function CreateFulfillmentConsumablesSupplyPage() { organizationName: p.organization.name, type: p.type || "NO_TYPE", })) || [], - supplierProducts: supplierProducts.map((p) => ({ + supplierProductsDetails: supplierProducts.slice(0, 5).map((p) => ({ id: p.id, name: p.name, organizationId: p.organization.id, organizationName: p.organization.name, })), }); - }, [selectedSupplier, productsData, productsLoading, supplierProducts]); + }, [ + selectedSupplier, + productsData, + productsLoading, + productsError, + supplierProducts.length, + ]); const formatCurrency = (amount: number) => { return new Intl.NumberFormat("ru-RU", { @@ -198,10 +235,13 @@ export function CreateFulfillmentConsumablesSupplyPage() { // 🔒 ВАЛИДАЦИЯ ОСТАТКОВ согласно правилам (раздел 6.2) if (quantity > 0) { - const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0); - + const availableStock = + (product.stock || product.quantity || 0) - (product.ordered || 0); + if (quantity > availableStock) { - toast.error(`❌ Недостаточно остатков!\nДоступно: ${availableStock} шт.\nЗапрашивается: ${quantity} шт.`); + toast.error( + `❌ Недостаточно остатков!\nДоступно: ${availableStock} шт.\nЗапрашивается: ${quantity} шт.` + ); return; } } @@ -265,7 +305,15 @@ export function CreateFulfillmentConsumablesSupplyPage() { !deliveryDate || !selectedLogistics ) { - toast.error("Заполните все обязательные поля"); + toast.error( + "Заполните все обязательные поля: поставщик, расходники, дата доставки и логистика" + ); + return; + } + + // Дополнительная проверка ID логистики + if (!selectedLogistics.id) { + toast.error("Выберите логистическую компанию"); return; } @@ -279,7 +327,7 @@ export function CreateFulfillmentConsumablesSupplyPage() { deliveryDate: deliveryDate, // Для фулфилмента указываем себя как получателя (поставка на свой склад) fulfillmentCenterId: user?.organization?.id, - logisticsPartnerId: selectedLogistics?.id, + logisticsPartnerId: selectedLogistics.id, // 🏷️ КЛАССИФИКАЦИЯ согласно правилам (раздел 2.2) consumableType: "FULFILLMENT_CONSUMABLES", // Расходники фулфилмента items: selectedConsumables.map((consumable) => ({ @@ -574,15 +622,19 @@ export function CreateFulfillmentConsumablesSupplyPage() {
{/* 🚫 ОВЕРЛЕЙ НЕДОСТУПНОСТИ */} {(() => { - const totalStock = product.stock || product.quantity || 0; + const totalStock = + product.stock || product.quantity || 0; const orderedStock = product.ordered || 0; - const availableStock = totalStock - orderedStock; - + const availableStock = + totalStock - orderedStock; + if (availableStock <= 0) { return (
-
НЕТ В НАЛИЧИИ
+
+ НЕТ В НАЛИЧИИ +
); @@ -636,10 +688,12 @@ export function CreateFulfillmentConsumablesSupplyPage() { )} {/* 🚨 ИНДИКАТОР НИЗКИХ ОСТАТКОВ согласно правилам (раздел 6.3) */} {(() => { - const totalStock = product.stock || product.quantity || 0; + const totalStock = + product.stock || product.quantity || 0; const orderedStock = product.ordered || 0; - const availableStock = totalStock - orderedStock; - + const availableStock = + totalStock - orderedStock; + if (availableStock <= 0) { return ( @@ -663,19 +717,26 @@ export function CreateFulfillmentConsumablesSupplyPage() { {/* 📊 АКТУАЛЬНЫЙ ОСТАТОК согласно правилам (раздел 6.4.2) */}
{(() => { - const totalStock = product.stock || product.quantity || 0; - const orderedStock = product.ordered || 0; - const availableStock = totalStock - orderedStock; - + const totalStock = + product.stock || + product.quantity || + 0; + const orderedStock = + product.ordered || 0; + const availableStock = + totalStock - orderedStock; + return (
- + Доступно: {availableStock} {orderedStock > 0 && ( @@ -693,10 +754,12 @@ export function CreateFulfillmentConsumablesSupplyPage() { {/* Управление количеством */}
{(() => { - const totalStock = product.stock || product.quantity || 0; + const totalStock = + product.stock || product.quantity || 0; const orderedStock = product.ordered || 0; - const availableStock = totalStock - orderedStock; - + const availableStock = + totalStock - orderedStock; + return (
- { - let inputValue = e.target.value; + { + let inputValue = e.target.value; - // Удаляем все нецифровые символы - inputValue = inputValue.replace( - /[^0-9]/g, - "" - ); + // Удаляем все нецифровые символы + inputValue = inputValue.replace( + /[^0-9]/g, + "" + ); - // Удаляем ведущие нули - inputValue = inputValue.replace( - /^0+/, - "" - ); + // Удаляем ведущие нули + inputValue = inputValue.replace( + /^0+/, + "" + ); - // Если строка пустая после удаления нулей, устанавливаем 0 - const numericValue = - inputValue === "" - ? 0 - : parseInt(inputValue); + // Если строка пустая после удаления нулей, устанавливаем 0 + const numericValue = + inputValue === "" + ? 0 + : parseInt(inputValue); - // Ограничиваем значение максимумом доступного остатка - const clampedValue = Math.min( - numericValue, - availableStock, - 99999 - ); + // Ограничиваем значение максимумом доступного остатка + const clampedValue = Math.min( + numericValue, + availableStock, + 99999 + ); - updateConsumableQuantity( - product.id, - clampedValue - ); - }} - onBlur={(e) => { - // При потере фокуса, если поле пустое, устанавливаем 0 - if (e.target.value === "") { - updateConsumableQuantity( - product.id, - 0 - ); - } - }} - className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50" - placeholder="0" - /> + updateConsumableQuantity( + product.id, + clampedValue + ); + }} + onBlur={(e) => { + // При потере фокуса, если поле пустое, устанавливаем 0 + if (e.target.value === "") { + updateConsumableQuantity( + product.id, + 0 + ); + } + }} + className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50" + placeholder="0" + />
diff --git a/src/components/fulfillment-supplies/fulfillment-supplies-dashboard.tsx b/src/components/fulfillment-supplies/fulfillment-supplies-dashboard.tsx index a6919e8..1d09d85 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies-dashboard.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies-dashboard.tsx @@ -1,22 +1,34 @@ "use client"; -import { useState } from "react"; +import React, { useState } from "react"; import { useQuery } from "@apollo/client"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card } from "@/components/ui/card"; import { Sidebar } from "@/components/dashboard/sidebar"; import { useSidebar } from "@/hooks/useSidebar"; import { GET_PENDING_SUPPLIES_COUNT } from "@/graphql/queries"; -import { Building2, ShoppingCart } from "lucide-react"; +import { + Building2, + ShoppingCart, + Package, + Wrench, + RotateCcw, + Clock, + FileText, + CheckCircle, +} from "lucide-react"; // Импорты компонентов подразделов import { FulfillmentSuppliesTab } from "./fulfillment-supplies/fulfillment-supplies-tab"; import { MarketplaceSuppliesTab } from "./marketplace-supplies/marketplace-supplies-tab"; +import { FulfillmentDetailedSuppliesTab } from "./fulfillment-supplies/fulfillment-detailed-supplies-tab"; +import { FulfillmentConsumablesOrdersTab } from "./fulfillment-supplies/fulfillment-consumables-orders-tab"; +import { PvzReturnsTab } from "./fulfillment-supplies/pvz-returns-tab"; // Компонент для отображения бейджа с уведомлениями function NotificationBadge({ count }: { count: number }) { if (count === 0) return null; - + return (
{count > 99 ? "99+" : count} @@ -27,72 +39,390 @@ function NotificationBadge({ count }: { count: number }) { export function FulfillmentSuppliesDashboard() { const { getSidebarMargin } = useSidebar(); const [activeTab, setActiveTab] = useState("fulfillment"); + const [activeSubTab, setActiveSubTab] = useState("goods"); // товар + const [activeThirdTab, setActiveThirdTab] = useState("new"); // новые // Загружаем данные о непринятых поставках - const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { - pollInterval: 30000, // Обновляем каждые 30 секунд - fetchPolicy: "cache-first", - errorPolicy: "ignore", - }); + const { data: pendingData, error: pendingError } = useQuery( + GET_PENDING_SUPPLIES_COUNT, + { + pollInterval: 30000, // Обновляем каждые 30 секунд + fetchPolicy: "cache-first", + errorPolicy: "ignore", + onError: (error) => { + console.error("❌ GET_PENDING_SUPPLIES_COUNT Error:", error); + }, + } + ); - const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; + // Логируем ошибку для диагностики + React.useEffect(() => { + if (pendingError) { + console.error("🚨 Ошибка загрузки счетчиков поставок:", pendingError); + } + }, [pendingError]); + + // ✅ ПРАВИЛЬНО: Для фулфилмента считаем только поставки, НЕ заявки на партнерство + const pendingCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0; + const ourSupplyOrdersCount = + pendingData?.pendingSuppliesCount?.ourSupplyOrders || 0; + const sellerSupplyOrdersCount = + pendingData?.pendingSuppliesCount?.sellerSupplyOrders || 0; return (
-
- {/* Основной контент с табами */} -
- - - + {/* БЛОК 1: ТАБЫ ВСЕХ УРОВНЕЙ */} +
+ {/* УРОВЕНЬ 1: Главные табы */} +
+
+ +
+
- - - - - + {/* УРОВЕНЬ 2: Подтабы */} + {activeTab === "fulfillment" && ( +
+
+ + + + +
+
+ )} - - - - - - + {/* УРОВЕНЬ 3: Подподтабы */} + {activeTab === "fulfillment" && activeSubTab === "goods" && ( +
+
+ + + +
+
+ )} +
+ + {/* БЛОК 2: МОДУЛИ СТАТИСТИКИ */} +
+

Статистика

+ + {/* Статистика для расходников фулфилмента */} + {activeTab === "fulfillment" && + activeSubTab === "detailed-supplies" && ( +
+
+
+ +
+

Наши заказы

+

+ {ourSupplyOrdersCount} +

+
+
+
+
+
+ +
+

Всего позиций

+

-

+
+
+
+
+
+ +
+

На складе

+

-

+
+
+
+
+
+ +
+

Доставлено

+

-

+
+
+
+
+ )} + + {/* Статистика для расходников селлеров */} + {activeTab === "fulfillment" && activeSubTab === "consumables" && ( +
+
+
+ +
+

От селлеров

+

+ {sellerSupplyOrdersCount} +

+
+
+
+
+
+ +
+

В обработке

+

-

+
+
+
+
+
+ +
+

Принято

+

-

+
+
+
+
+
+ +
+

Использовано

+

-

+
+
+
+
+ )} + + {/* Статистика для товаров */} + {activeTab === "fulfillment" && activeSubTab === "goods" && ( +
+
+
+ +
+

Новые

+

-

+
+
+
+
+
+ +
+

Приёмка

+

-

+
+
+
+
+
+ +
+

Принято

+

-

+
+
+
+
+ )} + + {/* Общая статистика для других разделов */} + {activeTab === "fulfillment" && activeSubTab === "returns" && ( +
Статистика возвратов с ПВЗ
+ )} + + {activeTab === "marketplace" && ( +
+ Статистика поставок на маркетплейсы +
+ )} +
+ + {/* БЛОК 3: ОСНОВНОЙ КОНТЕНТ */} +
+
+
+

+ Контент: {activeTab} → {activeSubTab} → {activeThirdTab} +

+ {/* КОНТЕНТ ДЛЯ ТОВАРОВ */} + {activeTab === "fulfillment" && + activeSubTab === "goods" && + activeThirdTab === "new" && ( +
+ Здесь отображаются НОВЫЕ поставки товаров на фулфилмент +
+ )} + {activeTab === "fulfillment" && + activeSubTab === "goods" && + activeThirdTab === "receiving" && ( +
+ Здесь отображаются товары в ПРИЁМКЕ +
+ )} + {activeTab === "fulfillment" && + activeSubTab === "goods" && + activeThirdTab === "received" && ( +
+ Здесь отображаются ПРИНЯТЫЕ товары +
+ )} + + {/* КОНТЕНТ ДЛЯ РАСХОДНИКОВ ФУЛФИЛМЕНТА */} + {activeTab === "fulfillment" && + activeSubTab === "detailed-supplies" && ( +
+ +
+ )} + + {/* КОНТЕНТ ДЛЯ РАСХОДНИКОВ СЕЛЛЕРОВ */} + {activeTab === "fulfillment" && + activeSubTab === "consumables" && ( +
+ +
+ )} + + {/* КОНТЕНТ ДЛЯ ВОЗВРАТОВ С ПВЗ */} + {activeTab === "fulfillment" && activeSubTab === "returns" && ( +
+ +
+ )} + + {/* КОНТЕНТ ДЛЯ МАРКЕТПЛЕЙСОВ */} + {activeTab === "marketplace" && ( +
+ Содержимое поставок на маркетплейсы +
+ )} +
+
diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx index 3d093e1..1abc341 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import React, { useState } from "react"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -12,8 +12,14 @@ import { GET_MY_SUPPLIES, GET_PENDING_SUPPLIES_COUNT, GET_WAREHOUSE_PRODUCTS, + GET_MY_EMPLOYEES, + GET_LOGISTICS_PARTNERS, } from "@/graphql/queries"; -import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations"; +import { + UPDATE_SUPPLY_ORDER_STATUS, + ASSIGN_LOGISTICS_TO_SUPPLY, + FULFILLMENT_RECEIVE_ORDER, +} from "@/graphql/mutations"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { @@ -34,16 +40,36 @@ import { Store, Bell, AlertTriangle, + UserPlus, + Settings, } from "lucide-react"; interface SupplyOrder { id: string; partnerId: string; deliveryDate: string; - status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED"; + status: + | "PENDING" + | "SUPPLIER_APPROVED" + | "CONFIRMED" + | "LOGISTICS_CONFIRMED" + | "SHIPPED" + | "IN_TRANSIT" + | "DELIVERED" + | "CANCELLED"; totalAmount: number; totalItems: number; createdAt: string; + fulfillmentCenter?: { + id: string; + name: string; + fullName: string; + }; + organization?: { + id: string; + name: string; + fullName: string; + }; partner: { id: string; inn: string; @@ -83,31 +109,106 @@ interface SupplyOrder { export function FulfillmentConsumablesOrdersTab() { const [expandedOrders, setExpandedOrders] = useState>(new Set()); + const [assigningOrders, setAssigningOrders] = useState>( + new Set() + ); + const [selectedLogistics, setSelectedLogistics] = useState<{ + [orderId: string]: string; + }>({}); + const [selectedEmployees, setSelectedEmployees] = useState<{ + [orderId: string]: string; + }>({}); const { user } = useAuth(); + // Запросы данных + const { + data: employeesData, + loading: employeesLoading, + error: employeesError, + } = useQuery(GET_MY_EMPLOYEES); + const { + data: logisticsData, + loading: logisticsLoading, + error: logisticsError, + } = useQuery(GET_LOGISTICS_PARTNERS); + + // Отладочная информация + console.log("DEBUG EMPLOYEES:", { + loading: employeesLoading, + error: employeesError?.message, + errorDetails: employeesError, + data: employeesData, + employees: employeesData?.myEmployees, + }); + console.log("DEBUG LOGISTICS:", { + loading: logisticsLoading, + error: logisticsError?.message, + errorDetails: logisticsError, + data: logisticsData, + partners: logisticsData?.logisticsPartners, + }); + + // Логируем ошибки отдельно + if (employeesError) { + console.error("EMPLOYEES ERROR:", employeesError); + } + if (logisticsError) { + console.error("LOGISTICS ERROR:", logisticsError); + } + // Загружаем заказы поставок const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS); - // Мутация для обновления статуса заказа - const [updateSupplyOrderStatus, { loading: updating }] = useMutation( - UPDATE_SUPPLY_ORDER_STATUS, + // Мутация для приемки поставки фулфилментом + const [fulfillmentReceiveOrder, { loading: receiving }] = useMutation( + FULFILLMENT_RECEIVE_ORDER, { onCompleted: (data) => { - if (data.updateSupplyOrderStatus.success) { - toast.success(data.updateSupplyOrderStatus.message); + if (data.fulfillmentReceiveOrder.success) { + toast.success(data.fulfillmentReceiveOrder.message); refetch(); // Обновляем список заказов } else { - toast.error(data.updateSupplyOrderStatus.message); + toast.error(data.fulfillmentReceiveOrder.message); } }, refetchQueries: [ { query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок { query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента (расходники фулфилмента) { query: GET_WAREHOUSE_PRODUCTS }, // Обновляем товары склада + { query: GET_PENDING_SUPPLIES_COUNT }, // Обновляем счетчики уведомлений ], onError: (error) => { - console.error("Error updating supply order status:", error); - toast.error("Ошибка при обновлении статуса заказа"); + console.error("Error receiving supply order:", error); + toast.error("Ошибка при приеме заказа поставки"); + }, + } + ); + + // Мутация для назначения логистики и ответственного + const [assignLogisticsToSupply, { loading: assigning }] = useMutation( + ASSIGN_LOGISTICS_TO_SUPPLY, + { + onCompleted: (data) => { + if (data.assignLogisticsToSupply.success) { + toast.success("Логистика и ответственный назначены успешно"); + refetch(); // Обновляем список заказов + // Сбрасываем состояние назначения + setAssigningOrders((prev) => { + const newSet = new Set(prev); + newSet.delete(data.assignLogisticsToSupply.supplyOrder.id); + return newSet; + }); + } else { + toast.error( + data.assignLogisticsToSupply.message || + "Ошибка при назначении логистики" + ); + } + }, + refetchQueries: [{ query: GET_SUPPLY_ORDERS }], + onError: (error) => { + console.error("Error assigning logistics:", error); + toast.error("Ошибка при назначении логистики"); }, } ); @@ -144,6 +245,19 @@ export function FulfillmentConsumablesOrdersTab() { number: fulfillmentOrders.length - index, // Обратный порядок для новых заказов сверху })); + // Автоматически открываем режим назначения для заказов, которые требуют назначения логистики + React.useEffect(() => { + fulfillmentOrders.forEach((order) => { + if (canAssignLogistics(order) && !assigningOrders.has(order.id)) { + setAssigningOrders((prev) => { + const newSet = new Set(prev); + newSet.add(order.id); + return newSet; + }); + } + }); + }, [fulfillmentOrders]); + const getStatusBadge = (status: SupplyOrder["status"]) => { const statusMap = { PENDING: { @@ -151,11 +265,26 @@ export function FulfillmentConsumablesOrdersTab() { color: "bg-blue-500/20 text-blue-300 border-blue-500/30", icon: Clock, }, - CONFIRMED: { - label: "Подтверждена", + SUPPLIER_APPROVED: { + label: "Одобрено", color: "bg-green-500/20 text-green-300 border-green-500/30", icon: CheckCircle, }, + CONFIRMED: { + label: "Подтверждена", + color: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30", + icon: CheckCircle, + }, + LOGISTICS_CONFIRMED: { + label: "Логистика OK", + color: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30", + icon: Truck, + }, + SHIPPED: { + label: "Отгружено", + color: "bg-orange-500/20 text-orange-300 border-orange-500/30", + icon: Package, + }, IN_TRANSIT: { label: "В пути", color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", @@ -181,24 +310,69 @@ export function FulfillmentConsumablesOrdersTab() { ); }; - const handleStatusUpdate = async ( - orderId: string, - newStatus: SupplyOrder["status"] - ) => { + // Функция для приема заказа фулфилментом + const handleReceiveOrder = async (orderId: string) => { try { - await updateSupplyOrderStatus({ - variables: { - id: orderId, - status: newStatus, - }, + await fulfillmentReceiveOrder({ + variables: { id: orderId }, }); } catch (error) { - console.error("Error updating status:", error); + console.error("Error receiving order:", error); } }; - const canMarkAsDelivered = (status: SupplyOrder["status"]) => { - return status === "IN_TRANSIT"; + // Проверяем, можно ли принять заказ (для фулфилмента) + const canReceiveOrder = (status: SupplyOrder["status"]) => { + return status === "SHIPPED"; + }; + + const toggleAssignmentMode = (orderId: string) => { + setAssigningOrders((prev) => { + const newSet = new Set(prev); + if (newSet.has(orderId)) { + newSet.delete(orderId); + } else { + newSet.add(orderId); + } + return newSet; + }); + }; + + const handleAssignLogistics = async (orderId: string) => { + const logisticsId = selectedLogistics[orderId]; + const employeeId = selectedEmployees[orderId]; + + if (!logisticsId) { + toast.error("Выберите логистическую компанию"); + return; + } + + if (!employeeId) { + toast.error("Выберите ответственного сотрудника"); + return; + } + + try { + await assignLogisticsToSupply({ + variables: { + supplyOrderId: orderId, + logisticsPartnerId: logisticsId, + responsibleId: employeeId, + }, + }); + } catch (error) { + console.error("Error assigning logistics:", error); + } + }; + + const canAssignLogistics = (order: SupplyOrder) => { + // Можем назначать логистику если: + // 1. Статус SUPPLIER_APPROVED (одобрено поставщиком) или CONFIRMED (подтвержден фулфилментом) + // 2. Логистика еще не назначена + return ( + (order.status === "SUPPLIER_APPROVED" || order.status === "CONFIRMED") && + !order.logisticsPartner + ); }; const formatDate = (dateString: string) => { @@ -247,15 +421,15 @@ export function FulfillmentConsumablesOrdersTab() {
-
- +
+
-

Ожидание

+

Одобрено

{ fulfillmentOrders.filter( - (order) => order.status === "PENDING" + (order) => order.status === "SUPPLIER_APPROVED" ).length }

@@ -265,8 +439,8 @@ export function FulfillmentConsumablesOrdersTab() {
-
- +
+

Подтверждено

@@ -336,8 +510,18 @@ export function FulfillmentConsumablesOrdersTab() { ordersWithNumbers.map((order) => ( toggleOrderExpansion(order.id)} + className={`bg-white/10 backdrop-blur border-white/20 overflow-hidden hover:bg-white/15 transition-colors ${ + canAssignLogistics(order) && assigningOrders.has(order.id) + ? "cursor-default" + : "cursor-pointer" + }`} + onClick={() => { + if ( + !(canAssignLogistics(order) && assigningOrders.has(order.id)) + ) { + toggleOrderExpansion(order.id); + } + }} > {/* Компактная основная информация */}
@@ -427,53 +611,20 @@ export function FulfillmentConsumablesOrdersTab() { {/* Правая часть - статус и действия */}
- - {order.status === "PENDING" && ( - - )} - {order.status === "CONFIRMED" && ( - - )} - {order.status === "IN_TRANSIT" && ( - - )} - {order.status === "DELIVERED" && ( - - )} - {order.status === "CANCELLED" && ( - - )} - {order.status === "PENDING" && "Ожидание"} - {order.status === "CONFIRMED" && "Подтверждена"} - {order.status === "IN_TRANSIT" && "В пути"} - {order.status === "DELIVERED" && "Доставлена"} - {order.status === "CANCELLED" && "Отменена"} - + {getStatusBadge(order.status)} - {canMarkAsDelivered(order.status) && ( + {canReceiveOrder(order.status) && ( )}
@@ -528,6 +679,100 @@ export function FulfillmentConsumablesOrdersTab() {
+ {/* Назначение логистики и ответственного в одной строке */} + {assigningOrders.has(order.id) && canAssignLogistics(order) && ( +
+
+ {/* Иконка и заголовок */} +
+ + Назначить: +
+ + {/* Выбор логистики */} +
+ +
+ + {/* Выбор ответственного */} +
+ +
+ + {/* Кнопки действий */} +
+ + +
+
+
+ )} + {/* Развернутые детали заказа */} {expandedOrders.has(order.id) && ( <> diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx index 1d8af74..008602c 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx @@ -33,8 +33,6 @@ import { CheckCircle, } from "lucide-react"; - - // Интерфейс для заказа interface SupplyOrder { id: string; @@ -174,10 +172,14 @@ export function FulfillmentDetailedSuppliesTab() { // "Расходники фулфилмента" = расходники, которые МЫ (фулфилмент-центр) заказали для себя // Критерии: создатель = мы И получатель = мы (ОБА условия) const ourSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( - (order: SupplyOrder) => { + (order: any) => { + // Защита от null/undefined значений return ( - order.organizationId === currentOrganizationId && // Создали мы - order.fulfillmentCenterId === currentOrganizationId // Получатель - мы + order?.organizationId === currentOrganizationId && // Создали мы + order?.fulfillmentCenterId === currentOrganizationId && // Получатель - мы + order?.organization && // Проверяем наличие organization + order?.partner && // Проверяем наличие partner + Array.isArray(order?.items) // Проверяем наличие items ); } ); @@ -248,7 +250,9 @@ export function FulfillmentDetailedSuppliesTab() { {/* Заголовок с кнопкой создания поставки */}
-

Расходники фулфилмента

+

+ Расходники фулфилмента +

Поставки расходников, поступающие на склад фулфилмент-центра

diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx index f4f1a39..9a02f3e 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx @@ -40,8 +40,6 @@ import { } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; - - // Интерфейсы для данных interface Employee { id: string; @@ -662,52 +660,50 @@ export function FulfillmentGoodsTab() { }; return ( -
- - {/* Вкладки товаров */} - - - - Новые - Н - - - - Приёмка - П - - - - Принято - Пр - - +
+ {/* УРОВЕНЬ 3: Подподтабы (маленький размер, больший отступ) */} +
+ + + + + Новые + Н + + + + Приёмка + П + + + + Принято + Пр + + - - - + + + - - - + + + - - - - + + + + +
); diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx index 2b72bcd..d5733ea 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import { useQuery } from "@apollo/client"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -32,13 +32,33 @@ export function FulfillmentSuppliesTab() { const [activeTab, setActiveTab] = useState("goods"); // Загружаем данные о непринятых поставках - const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { - pollInterval: 30000, // Обновляем каждые 30 секунд - fetchPolicy: "cache-first", - errorPolicy: "ignore", - }); + const { data: pendingData, error: pendingError } = useQuery( + GET_PENDING_SUPPLIES_COUNT, + { + pollInterval: 30000, // Обновляем каждые 30 секунд + fetchPolicy: "cache-first", + errorPolicy: "ignore", + onError: (error) => { + console.error( + "❌ GET_PENDING_SUPPLIES_COUNT Error in FulfillmentSuppliesTab:", + error + ); + }, + } + ); - const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; + // Логируем ошибку для диагностики + React.useEffect(() => { + if (pendingError) { + console.error( + "🚨 Ошибка загрузки счетчиков в FulfillmentSuppliesTab:", + pendingError + ); + } + }, [pendingError]); + + // ✅ ПРАВИЛЬНО: Для фулфилмента считаем только поставки, НЕ заявки на партнерство + const pendingCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0; const ourSupplyOrdersCount = pendingData?.pendingSuppliesCount?.ourSupplyOrders || 0; const sellerSupplyOrdersCount = @@ -66,74 +86,74 @@ export function FulfillmentSuppliesTab() { }; return ( -
- - - - - Товар - Т - - - - Расходники фулфилмента - Фулфилмент - Ф - - - - - Расходники селлеров - Селлеры - С - - - - - Возвраты с ПВЗ - В - - - - - - - - + {/* УРОВЕНЬ 2: Подтабы (средний размер, отступ показывает иерархию) */} +
+ -
- -
- + + + + Товар + Т + + + + Расходники фулфилмента + Фулфилмент + Ф + + + + + Расходники селлеров + Селлеры + С + + + + + Возвраты с ПВЗ + В + + - -
- -
-
+ + + - - - -
+ +
+ +
+
+ + +
+ +
+
+ + + + + +
); } diff --git a/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx b/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx index 4d357cb..9b4f3f7 100644 --- a/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx +++ b/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx @@ -7,6 +7,11 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Sidebar } from "@/components/dashboard/sidebar"; import { useSidebar } from "@/hooks/useSidebar"; import { useAuth } from "@/hooks/useAuth"; @@ -15,7 +20,8 @@ import { GET_MY_COUNTERPARTIES, GET_SUPPLY_ORDERS, GET_WAREHOUSE_PRODUCTS, - GET_MY_SUPPLIES, // Расходники селлеров + GET_MY_SUPPLIES, // Расходники селлеров (старые данные заказов) + GET_SELLER_SUPPLIES_ON_WAREHOUSE, // Расходники селлеров на складе (новый API) GET_MY_FULFILLMENT_SUPPLIES, // Расходники фулфилмента GET_FULFILLMENT_WAREHOUSE_STATS, // Статистика склада с изменениями за сутки } from "@/graphql/queries"; @@ -55,6 +61,7 @@ interface ProductVariant { defectsQuantity: number; sellerSuppliesPlace?: string; sellerSuppliesQuantity: number; + sellerSuppliesOwners?: string[]; // Владельцы расходников pvzReturnsPlace?: string; pvzReturnsQuantity: number; } @@ -72,6 +79,7 @@ interface ProductItem { defectsQuantity: number; sellerSuppliesPlace?: string; sellerSuppliesQuantity: number; + sellerSuppliesOwners?: string[]; // Владельцы расходников pvzReturnsPlace?: string; pvzReturnsQuantity: number; // Третий уровень - варианты товара @@ -200,13 +208,13 @@ export function FulfillmentWarehouseDashboard() { fetchPolicy: "cache-and-network", }); - // Загружаем расходники селлеров + // Загружаем расходники селлеров на складе фулфилмента const { - data: suppliesData, - loading: suppliesLoading, - error: suppliesError, - refetch: refetchSupplies, - } = useQuery(GET_MY_SUPPLIES, { + data: sellerSuppliesData, + loading: sellerSuppliesLoading, + error: sellerSuppliesError, + refetch: refetchSellerSupplies, + } = useQuery(GET_SELLER_SUPPLIES_ON_WAREHOUSE, { fetchPolicy: "cache-and-network", }); @@ -246,8 +254,10 @@ export function FulfillmentWarehouseDashboard() { goods: warehouseStatsData.fulfillmentWarehouseStats.goods, defects: warehouseStatsData.fulfillmentWarehouseStats.defects, pvzReturns: warehouseStatsData.fulfillmentWarehouseStats.pvzReturns, - fulfillmentSupplies: warehouseStatsData.fulfillmentWarehouseStats.fulfillmentSupplies, - sellerSupplies: warehouseStatsData.fulfillmentWarehouseStats.sellerSupplies, + fulfillmentSupplies: + warehouseStatsData.fulfillmentWarehouseStats.fulfillmentSupplies, + sellerSupplies: + warehouseStatsData.fulfillmentWarehouseStats.sellerSupplies, }); } @@ -258,7 +268,7 @@ export function FulfillmentWarehouseDashboard() { ); const supplyOrders: SupplyOrder[] = ordersData?.supplyOrders || []; const allProducts = productsData?.warehouseProducts || []; - const mySupplies = suppliesData?.mySupplies || []; // Расходники селлеров + const sellerSupplies = sellerSuppliesData?.sellerSuppliesOnWarehouse || []; // Расходники селлеров на складе const myFulfillmentSupplies = fulfillmentSuppliesData?.myFulfillmentSupplies || []; // Расходники фулфилмента @@ -276,8 +286,8 @@ export function FulfillmentWarehouseDashboard() { deliveredOrders: supplyOrders.filter((o) => o.status === "DELIVERED") .length, productsCount: allProducts.length, - suppliesCount: mySupplies.length, // Добавляем логирование расходников - supplies: mySupplies.map((s: any) => ({ + suppliesCount: sellerSupplies.length, // Добавляем логирование расходников + supplies: sellerSupplies.map((s: any) => ({ id: s.id, name: s.name, currentStock: s.currentStock, @@ -293,7 +303,7 @@ export function FulfillmentWarehouseDashboard() { })), // Добавляем анализ соответствия товаров и расходников productSupplyMatching: allProducts.map((product: any) => { - const matchingSupply = mySupplies.find((supply: any) => { + const matchingSupply = sellerSupplies.find((supply: any) => { return ( supply.name.toLowerCase() === product.name.toLowerCase() || supply.name @@ -311,11 +321,11 @@ export function FulfillmentWarehouseDashboard() { counterpartiesLoading, ordersLoading, productsLoading, - suppliesLoading, // Добавляем статус загрузки расходников + sellerSuppliesLoading, // Добавляем статус загрузки расходников селлеров counterpartiesError: counterpartiesError?.message, ordersError: ordersError?.message, productsError: productsError?.message, - suppliesError: suppliesError?.message, // Добавляем ошибки загрузки расходников + sellerSuppliesError: sellerSuppliesError?.message, // Добавляем ошибки загрузки расходников селлеров }); // Расчет поступлений расходников за сутки (выносим отдельно для использования в storeData) @@ -408,7 +418,7 @@ export function FulfillmentWarehouseDashboard() { console.log("📊 Статистика расходников селлера:", { suppliesReceivedToday, suppliesUsedToday, - totalSellerSupplies: mySupplies.reduce( + totalSellerSupplies: sellerSupplies.reduce( (sum: number, supply: any) => sum + (supply.currentStock || 0), 0 ), @@ -418,7 +428,10 @@ export function FulfillmentWarehouseDashboard() { // Получаем статистику склада из GraphQL (с реальными изменениями за сутки) const warehouseStats: WarehouseStats = useMemo(() => { // Если данные еще загружаются, возвращаем нули - if (warehouseStatsLoading || !warehouseStatsData?.fulfillmentWarehouseStats) { + if ( + warehouseStatsLoading || + !warehouseStatsData?.fulfillmentWarehouseStats + ) { return { products: { current: 0, change: 0 }, goods: { current: 0, change: 0 }, @@ -511,26 +524,64 @@ export function FulfillmentWarehouseDashboard() { } }); - // Группируем расходники по названию - const groupedSupplies = new Map(); - mySupplies.forEach((supply: any) => { + // ИСПРАВЛЕНО: Группируем расходники по СЕЛЛЕРУ-ВЛАДЕЛЬЦУ, а не по названию + const suppliesByOwner = new Map< + string, + Map + >(); + + sellerSupplies.forEach((supply: any) => { + const ownerId = supply.sellerOwner?.id; + const ownerName = + supply.sellerOwner?.name || + supply.sellerOwner?.fullName || + "Неизвестный селлер"; const supplyName = supply.name; const currentStock = supply.currentStock || 0; + const supplyType = supply.type; - if (groupedSupplies.has(supplyName)) { - groupedSupplies.set( - supplyName, - groupedSupplies.get(supplyName)! + currentStock + // ИСПРАВЛЕНО: Строгая проверка согласно правилам + if (!ownerId || supplyType !== "SELLER_CONSUMABLES") { + console.warn( + "⚠️ ОТФИЛЬТРОВАН расходник в компоненте (нарушение правил):", + { + id: supply.id, + name: supplyName, + type: supplyType, + ownerId, + ownerName, + reason: !ownerId + ? "нет sellerOwner.id" + : "тип не SELLER_CONSUMABLES", + } ); + return; // Пропускаем согласно ПРАВИЛУ 6 из секции 11.6 + } + + // Инициализируем группу для селлера, если её нет + if (!suppliesByOwner.has(ownerId)) { + suppliesByOwner.set(ownerId, new Map()); + } + + const ownerSupplies = suppliesByOwner.get(ownerId)!; + + if (ownerSupplies.has(supplyName)) { + // Суммируем количество, если расходник уже есть у этого селлера + const existing = ownerSupplies.get(supplyName)!; + existing.quantity += currentStock; } else { - groupedSupplies.set(supplyName, currentStock); + // Добавляем новый расходник для этого селлера + ownerSupplies.set(supplyName, { + quantity: currentStock, + ownerName: ownerName, + }); } }); // Логирование группировки console.log("📊 Группировка товаров и расходников:", { groupedProductsCount: groupedProducts.size, - groupedSuppliesCount: groupedSupplies.size, + suppliesByOwnerCount: suppliesByOwner.size, groupedProducts: Array.from(groupedProducts.entries()).map( ([name, data]) => ({ name, @@ -539,10 +590,20 @@ export function FulfillmentWarehouseDashboard() { uniqueSuppliers: [...new Set(data.suppliers)], }) ), - groupedSupplies: Array.from(groupedSupplies.entries()).map( - ([name, quantity]) => ({ - name, - totalQuantity: quantity, + suppliesByOwner: Array.from(suppliesByOwner.entries()).map( + ([ownerId, ownerSupplies]) => ({ + ownerId, + suppliesCount: ownerSupplies.size, + totalQuantity: Array.from(ownerSupplies.values()).reduce( + (sum, s) => sum + s.quantity, + 0 + ), + ownerName: + Array.from(ownerSupplies.values())[0]?.ownerName || "Unknown", + supplies: Array.from(ownerSupplies.entries()).map(([name, data]) => ({ + name, + quantity: data.quantity, + })), }) ), }); @@ -567,37 +628,56 @@ export function FulfillmentWarehouseDashboard() { const productData = groupedProducts.get(productName)!; const itemProducts = productData.totalQuantity; - // Ищем соответствующий расходник по названию - const matchingSupplyQuantity = groupedSupplies.get(productName) || 0; + // ИСПРАВЛЕНО: Ищем расходники конкретного селлера-владельца + let itemSuppliesQuantity = 0; + let suppliesOwners: string[] = []; - // Если нет точного совпадения, ищем частичное совпадение - let itemSuppliesQuantity = matchingSupplyQuantity; - if (itemSuppliesQuantity === 0) { - for (const [supplyName, quantity] of groupedSupplies.entries()) { - if ( - supplyName.toLowerCase().includes(productName.toLowerCase()) || - productName.toLowerCase().includes(supplyName.toLowerCase()) - ) { - itemSuppliesQuantity = quantity; - break; + // Получаем реального селлера для этого виртуального партнера + const realSeller = sellerPartners[index]; + + if (realSeller?.id && suppliesByOwner.has(realSeller.id)) { + const sellerSupplies = suppliesByOwner.get(realSeller.id)!; + + // Ищем расходники этого селлера по названию товара + const matchingSupply = sellerSupplies.get(productName); + + if (matchingSupply) { + itemSuppliesQuantity = matchingSupply.quantity; + suppliesOwners = [matchingSupply.ownerName]; + } else { + // Если нет точного совпадения, ищем частичное среди расходников ЭТОГО селлера + for (const [supplyName, supplyData] of sellerSupplies.entries()) { + if ( + supplyName + .toLowerCase() + .includes(productName.toLowerCase()) || + productName.toLowerCase().includes(supplyName.toLowerCase()) + ) { + itemSuppliesQuantity = supplyData.quantity; + suppliesOwners = [supplyData.ownerName]; + break; + } } } } - // Fallback к процентному соотношению - if (itemSuppliesQuantity === 0) { - itemSuppliesQuantity = Math.floor(itemProducts * 0.1); - } + // Если у этого селлера нет расходников для данного товара - оставляем 0 + // НЕ используем fallback, так как должны показывать только реальные данные - console.log(`📦 Товар "${productName}":`, { - totalQuantity: itemProducts, - suppliersCount: productData.suppliers.length, - uniqueSuppliers: [...new Set(productData.suppliers)], - matchingSupplyQuantity: matchingSupplyQuantity, - finalSuppliesQuantity: itemSuppliesQuantity, - usedFallback: - matchingSupplyQuantity === 0 && itemSuppliesQuantity > 0, - }); + console.log( + `📦 Товар "${productName}" (партнер: ${ + realSeller?.name || "Unknown" + }):`, + { + totalQuantity: itemProducts, + suppliersCount: productData.suppliers.length, + uniqueSuppliers: [...new Set(productData.suppliers)], + sellerSuppliesQuantity: itemSuppliesQuantity, + suppliesOwners: suppliesOwners, + sellerId: realSeller?.id, + hasSellerSupplies: itemSuppliesQuantity > 0, + } + ); return { id: `grouped-${productName}-${itemIndex}`, // Уникальный ID для группированного товара @@ -615,6 +695,7 @@ export function FulfillmentWarehouseDashboard() { defectsQuantity: 0, // Нет реальных данных о браке sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}`, sellerSuppliesQuantity: itemSuppliesQuantity, // Суммированное количество расходников (реальные данные) + sellerSuppliesOwners: suppliesOwners, // Владельцы расходников (ИСПРАВЛЕНО) pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}`, pvzReturnsQuantity: 0, // Нет реальных данных о возвратах с ПВЗ // Создаем варианты товара @@ -634,6 +715,7 @@ export function FulfillmentWarehouseDashboard() { sellerSuppliesQuantity: Math.floor( itemSuppliesQuantity * 0.4 ), // Часть от расходников + sellerSuppliesOwners: suppliesOwners, // Владельцы расходников (ИСПРАВЛЕНО) pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-1`, pvzReturnsQuantity: 0, // Нет реальных данных о возвратах }, @@ -650,6 +732,7 @@ export function FulfillmentWarehouseDashboard() { sellerSuppliesQuantity: Math.floor( itemSuppliesQuantity * 0.4 ), // Часть от расходников + sellerSuppliesOwners: suppliesOwners, // Владельцы расходников (ИСПРАВЛЕНО) pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-2`, pvzReturnsQuantity: 0, // Нет реальных данных о возвратах }, @@ -666,6 +749,7 @@ export function FulfillmentWarehouseDashboard() { sellerSuppliesQuantity: Math.floor( itemSuppliesQuantity * 0.2 ), // Оставшаяся часть расходников + sellerSuppliesOwners: suppliesOwners, // Владельцы расходников (ИСПРАВЛЕНО) pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-3`, pvzReturnsQuantity: 0, // Нет реальных данных о возвратах }, @@ -738,7 +822,7 @@ export function FulfillmentWarehouseDashboard() { totalSellerSupplies > 0 ? Math.floor( (totalSellerSupplies / - (mySupplies.reduce( + (sellerSupplies.reduce( (sum: number, supply: any) => sum + (supply.currentStock || 0), 0 @@ -774,7 +858,7 @@ export function FulfillmentWarehouseDashboard() { items, }; }); - }, [sellerPartners, allProducts, mySupplies, suppliesReceivedToday]); + }, [sellerPartners, allProducts, sellerSupplies, suppliesReceivedToday]); // Функции для аватаров магазинов const getInitials = (name: string): string => { @@ -1007,9 +1091,14 @@ export function FulfillmentWarehouseDashboard() { onClick?: () => void; }) => { // Используем percentChange из GraphQL, если доступно, иначе вычисляем локально - const displayPercentChange = percentChange !== undefined && percentChange !== null && !isNaN(percentChange) - ? percentChange - : (current > 0 ? (change / current) * 100 : 0); + const displayPercentChange = + percentChange !== undefined && + percentChange !== null && + !isNaN(percentChange) + ? percentChange + : current > 0 + ? (change / current) * 100 + : 0; return (
@@ -1210,7 +1299,7 @@ export function FulfillmentWarehouseDashboard() { counterpartiesLoading || ordersLoading || productsLoading || - suppliesLoading + sellerSuppliesLoading } > @@ -1224,7 +1313,10 @@ export function FulfillmentWarehouseDashboard() { icon={Box} current={warehouseStats.products.current} change={warehouseStats.products.change} - percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.products?.percentChange} + percentChange={ + warehouseStatsData?.fulfillmentWarehouseStats?.products + ?.percentChange + } description="Готовые к отправке" /> router.push("/fulfillment-warehouse/supplies")} /> @@ -1265,7 +1369,10 @@ export function FulfillmentWarehouseDashboard() { icon={Users} current={warehouseStats.sellerSupplies.current} change={warehouseStats.sellerSupplies.change} - percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.sellerSupplies?.percentChange} + percentChange={ + warehouseStatsData?.fulfillmentWarehouseStats?.sellerSupplies + ?.percentChange + } description="Материалы клиентов" />
@@ -1935,11 +2042,43 @@ export function FulfillmentWarehouseDashboard() { {/* Расходники селлера */}
-
- {formatNumber( - item.sellerSuppliesQuantity - )} -
+ + +
+ {formatNumber( + item.sellerSuppliesQuantity + )} +
+
+ +
+
+ Расходники селлеров: +
+ {item.sellerSuppliesOwners && + item.sellerSuppliesOwners.length > + 0 ? ( +
+ {item.sellerSuppliesOwners.map( + (owner, i) => ( +
+
+ {owner} +
+ ) + )} +
+ ) : ( +
+ Нет данных о владельцах +
+ )} +
+
+
{item.sellerSuppliesPlace || "-"}
@@ -2065,11 +2204,45 @@ export function FulfillmentWarehouseDashboard() { {/* Расходники селлера */}
-
- {formatNumber( - variant.sellerSuppliesQuantity - )} -
+ + +
+ {formatNumber( + variant.sellerSuppliesQuantity + )} +
+
+ +
+
+ Расходники селлеров: +
+ {variant.sellerSuppliesOwners && + variant + .sellerSuppliesOwners + .length > 0 ? ( +
+ {variant.sellerSuppliesOwners.map( + (owner, i) => ( +
+
+ {owner} +
+ ) + )} +
+ ) : ( +
+ Нет данных о + владельцах +
+ )} +
+
+
{variant.sellerSuppliesPlace || "-"} diff --git a/src/components/logistics-orders/logistics-orders-dashboard.tsx b/src/components/logistics-orders/logistics-orders-dashboard.tsx index feab6cd..4fac662 100644 --- a/src/components/logistics-orders/logistics-orders-dashboard.tsx +++ b/src/components/logistics-orders/logistics-orders-dashboard.tsx @@ -11,9 +11,9 @@ import { Sidebar } from "@/components/dashboard/sidebar"; import { useSidebar } from "@/hooks/useSidebar"; import { useAuth } from "@/hooks/useAuth"; import { GET_SUPPLY_ORDERS } from "@/graphql/queries"; -import { - LOGISTICS_CONFIRM_ORDER, - LOGISTICS_REJECT_ORDER +import { + LOGISTICS_CONFIRM_ORDER, + LOGISTICS_REJECT_ORDER, } from "@/graphql/mutations"; import { toast } from "sonner"; import { @@ -37,7 +37,15 @@ interface SupplyOrder { organizationId: string; partnerId: string; deliveryDate: string; - status: "PENDING" | "SUPPLIER_APPROVED" | "LOGISTICS_CONFIRMED" | "SHIPPED" | "DELIVERED" | "CANCELLED"; + status: + | "PENDING" + | "SUPPLIER_APPROVED" + | "CONFIRMED" + | "LOGISTICS_CONFIRMED" + | "SHIPPED" + | "IN_TRANSIT" + | "DELIVERED" + | "CANCELLED"; totalAmount: number; totalItems: number; createdAt: string; @@ -89,7 +97,11 @@ export function LogisticsOrdersDashboard() { fetchPolicy: "cache-and-network", }); - console.log(`DEBUG ЛОГИСТИКА: loading=${loading}, error=${error?.message}, totalOrders=${data?.supplyOrders?.length || 0}`); + console.log( + `DEBUG ЛОГИСТИКА: loading=${loading}, error=${ + error?.message + }, totalOrders=${data?.supplyOrders?.length || 0}` + ); // Мутации для действий логистики const [logisticsConfirmOrder] = useMutation(LOGISTICS_CONFIRM_ORDER, { @@ -137,8 +149,15 @@ export function LogisticsOrdersDashboard() { // Фильтруем заказы где текущая организация является логистическим партнером const logisticsOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( (order: SupplyOrder) => { - const isLogisticsPartner = order.logisticsPartner?.id === user?.organization?.id; - console.log(`DEBUG ЛОГИСТИКА: Заказ ${order.id.slice(-8)} - статус: ${order.status}, logisticsPartnerId: ${order.logisticsPartner?.id}, currentOrgId: ${user?.organization?.id}, isLogisticsPartner: ${isLogisticsPartner}`); + const isLogisticsPartner = + order.logisticsPartner?.id === user?.organization?.id; + console.log( + `DEBUG ЛОГИСТИКА: Заказ ${order.id.slice(-8)} - статус: ${ + order.status + }, logisticsPartnerId: ${order.logisticsPartner?.id}, currentOrgId: ${ + user?.organization?.id + }, isLogisticsPartner: ${isLogisticsPartner}` + ); return isLogisticsPartner; } ); @@ -155,6 +174,11 @@ export function LogisticsOrdersDashboard() { color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", icon: AlertTriangle, }, + CONFIRMED: { + label: "Требует подтверждения", + color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + icon: AlertTriangle, + }, LOGISTICS_CONFIRMED: { label: "Подтверждено", color: "bg-blue-500/20 text-blue-300 border-blue-500/30", @@ -187,7 +211,7 @@ export function LogisticsOrdersDashboard() { icon: Truck, }, }; - + const config = statusMap[status as keyof typeof statusMap]; if (!config) { console.warn(`Unknown status: ${status}`); @@ -199,7 +223,7 @@ export function LogisticsOrdersDashboard() { ); } - + const { label, color, icon: Icon } = config; return ( @@ -247,7 +271,9 @@ export function LogisticsOrdersDashboard() { return (
-
+
Загрузка заказов...
@@ -260,9 +286,13 @@ export function LogisticsOrdersDashboard() { return (
-
+
-
Ошибка загрузки заказов: {error.message}
+
+ Ошибка загрузки заказов: {error.message} +
@@ -272,7 +302,9 @@ export function LogisticsOrdersDashboard() { return (
-
+
{/* Заголовок */}
@@ -296,7 +328,13 @@ export function LogisticsOrdersDashboard() {

Требуют подтверждения

- {logisticsOrders.filter(order => order.status === "SUPPLIER_APPROVED").length} + { + logisticsOrders.filter( + (order) => + order.status === "SUPPLIER_APPROVED" || + order.status === "CONFIRMED" + ).length + }

@@ -310,7 +348,11 @@ export function LogisticsOrdersDashboard() {

Подтверждено

- {logisticsOrders.filter(order => order.status === "LOGISTICS_CONFIRMED").length} + { + logisticsOrders.filter( + (order) => order.status === "LOGISTICS_CONFIRMED" + ).length + }

@@ -324,7 +366,11 @@ export function LogisticsOrdersDashboard() {

В пути

- {logisticsOrders.filter(order => order.status === "SHIPPED").length} + { + logisticsOrders.filter( + (order) => order.status === "SHIPPED" + ).length + }

@@ -338,7 +384,11 @@ export function LogisticsOrdersDashboard() {

Доставлено

- {logisticsOrders.filter(order => order.status === "DELIVERED").length} + { + logisticsOrders.filter( + (order) => order.status === "DELIVERED" + ).length + }

@@ -355,7 +405,8 @@ export function LogisticsOrdersDashboard() { Нет логистических заказов

- Заказы поставок, требующие логистического сопровождения, будут отображаться здесь + Заказы поставок, требующие логистического сопровождения, + будут отображаться здесь

@@ -384,19 +435,29 @@ export function LogisticsOrdersDashboard() {
- {getInitials(order.partner.name || order.partner.fullName || "П")} + {getInitials( + order.partner.name || + order.partner.fullName || + "П" + )} - {getInitials(order.organization.name || order.organization.fullName || "ФФ")} + {getInitials( + order.organization.name || + order.organization.fullName || + "ФФ" + )}

- {order.partner.name || order.partner.fullName} → {order.organization.name || order.organization.fullName} + {order.partner.name || order.partner.fullName} →{" "} + {order.organization.name || + order.organization.fullName}

Поставщик → Фулфилмент @@ -426,7 +487,8 @@ export function LogisticsOrdersDashboard() { {getStatusBadge(order.status)} {/* Кнопки действий для логистики */} - {order.status === "SUPPLIER_APPROVED" && ( + {(order.status === "SUPPLIER_APPROVED" || + order.status === "CONFIRMED") && (

+ + {/* Действия для PENDING */} + {order.status === "PENDING" && ( + <> + + + + )} + + {/* Действие для LOGISTICS_CONFIRMED */} + {order.status === "LOGISTICS_CONFIRMED" && ( + + )} + + {/* Кнопка связаться всегда доступна */} + +
+
+ + {/* Срок доставки */} +
+
+
+ + Доставка: + Склад фулфилмента +
+
+ + Срок: + + {formatDate(order.deliveryDate)} + +
+
+
+
+ + {/* Расширенная детализация */} + {isExpanded && ( +
+

+ 📋 ДЕТАЛИ ЗАЯВКИ #{order.id.slice(-8)} +

+ + {/* Товары в заявке */} +
+
+ 📦 ТОВАРЫ В ЗАЯВКЕ: +
+
+ {order.items.map((item) => ( +
+
+ + {item.product.name} • {item.quantity} шт • {item.price} + ₽/шт = {item.totalPrice.toLocaleString()}₽ + +
+ Артикул: {item.product.article} + {item.product.category && + ` • ${item.product.category.name}`} +
+
+
+ ))} +
+ + Общая стоимость: {order.totalAmount.toLocaleString()}₽ + +
+
+
+ + {/* Логистическая информация */} +
+
+ 📍 ЛОГИСТИЧЕСКАЯ ИНФОРМАЦИЯ: +
+
+
+ • Объем груза: {calculateVolume()} м³ +
+
+ • Предварительная стоимость доставки: ~ + {Math.round( + parseFloat(calculateVolume()) * 3500 + ).toLocaleString()} + ₽ +
+
+ • Маршрут: Склад поставщика →{" "} + {order.fulfillmentCenter?.name || "Фулфилмент-центр"} +
+
+
+ + {/* Контактная информация */} +
+
📞 КОНТАКТЫ:
+
+
+ • Заказчик:{" "} + {order.organization.name || order.organization.fullName} + {order.organization.inn && + ` (ИНН: ${order.organization.inn})`} +
+ {order.fulfillmentCenter && ( +
+ • Фулфилмент:{" "} + {order.fulfillmentCenter.name || + order.fulfillmentCenter.fullName} +
+ )} +
+
+
+ )} + + + {/* Модал отклонения заявки */} + + + + Отклонить заявку + +
+
+ +