Добавлено новое поле fulfillmentCenterId в модель SupplyOrder и соответствующий реляционный объект fulfillmentCenter для улучшения обработки заказов. Обновлены компоненты FulfillmentSuppliesTab и RealSupplyOrdersTab для интеграции нового функционала. Оптимизированы стили и структура кода для повышения удобства использования.

This commit is contained in:
Bivekich
2025-07-24 15:10:58 +03:00
parent 74fb071552
commit c6b1b15c80
11 changed files with 481 additions and 47 deletions

View File

@ -0,0 +1,410 @@
"use client";
import React, { useState } from "react";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { StatsCard } from "../../supplies/ui/stats-card";
import { StatsGrid } from "../../supplies/ui/stats-grid";
import { useQuery } from "@apollo/client";
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
import { useAuth } from "@/hooks/useAuth";
import {
Calendar,
Building2,
TrendingUp,
DollarSign,
Wrench,
Package2,
ChevronDown,
ChevronRight,
User,
} from "lucide-react";
interface SupplyOrderItem {
id: string;
quantity: number;
price: number;
totalPrice: number;
product: {
id: string;
name: string;
article: string;
description?: string;
category?: {
id: string;
name: string;
};
};
}
interface SupplyOrder {
id: string;
deliveryDate: string;
status: string;
totalAmount: number;
totalItems: number;
fulfillmentCenterId?: string;
createdAt: string;
updatedAt: string;
partner: {
id: string;
name?: string;
fullName?: string;
inn: string;
address?: string;
phones?: string[];
emails?: string[];
};
organization: {
id: string;
name?: string;
fullName?: string;
type: string;
};
fulfillmentCenter?: {
id: string;
name?: string;
fullName?: string;
type: string;
};
items: SupplyOrderItem[];
}
export function FulfillmentConsumablesOrdersTab() {
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
const { user } = useAuth();
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS);
// Получаем ID текущей организации (фулфилмент-центра)
const currentOrganizationId = user?.organization?.id;
// Фильтруем заказы где текущая организация является получателем
const incomingSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter(
(order: SupplyOrder) => {
// Показываем заказы где текущий фулфилмент-центр указан как получатель
return order.fulfillmentCenterId === currentOrganizationId;
}
);
const toggleOrderExpansion = (orderId: string) => {
const newExpanded = new Set(expandedOrders);
if (newExpanded.has(orderId)) {
newExpanded.delete(orderId);
} else {
newExpanded.add(orderId);
}
setExpandedOrders(newExpanded);
};
const getStatusBadge = (status: string) => {
const statusMap: Record<string, { label: string; color: string }> = {
CREATED: {
label: "Новый заказ",
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
},
CONFIRMED: {
label: "Подтвержден",
color: "bg-green-500/20 text-green-300 border-green-500/30",
},
IN_PROGRESS: {
label: "Обрабатывается",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
},
DELIVERED: {
label: "Доставлен",
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
},
CANCELLED: {
label: "Отменен",
color: "bg-red-500/20 text-red-300 border-red-500/30",
},
};
const { label, color } = statusMap[status] || {
label: status,
color: "bg-gray-500/20 text-gray-300 border-gray-500/30",
};
return <Badge className={`${color} border`}>{label}</Badge>;
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
}).format(amount);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
};
const formatDateTime = (dateString: string) => {
return new Date(dateString).toLocaleString("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
// Статистика для фулфилмент-центра
const totalOrders = incomingSupplyOrders.length;
const totalAmount = incomingSupplyOrders.reduce((sum, order) => sum + order.totalAmount, 0);
const totalItems = incomingSupplyOrders.reduce((sum, order) => sum + order.totalItems, 0);
const newOrders = incomingSupplyOrders.filter(order => order.status === "CREATED").length;
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
<span className="ml-3 text-white/60">Загрузка заказов расходников...</span>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<Wrench className="h-12 w-12 text-red-400 mx-auto mb-4" />
<p className="text-red-400 font-medium">Ошибка загрузки заказов</p>
<p className="text-white/60 text-sm mt-2">{error.message}</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Статистика входящих заказов расходников */}
<StatsGrid>
<StatsCard
title="Входящие заказы"
value={totalOrders}
icon={Package2}
iconColor="text-orange-400"
iconBg="bg-orange-500/20"
subtitle="Заказы от селлеров"
/>
<StatsCard
title="Общая сумма"
value={formatCurrency(totalAmount)}
icon={TrendingUp}
iconColor="text-green-400"
iconBg="bg-green-500/20"
subtitle="Стоимость заказов"
/>
<StatsCard
title="Всего единиц"
value={totalItems}
icon={Wrench}
iconColor="text-blue-400"
iconBg="bg-blue-500/20"
subtitle="Количество расходников"
/>
<StatsCard
title="Новые заказы"
value={newOrders}
icon={Calendar}
iconColor="text-purple-400"
iconBg="bg-purple-500/20"
subtitle="Требуют обработки"
/>
</StatsGrid>
{/* Список входящих заказов расходников */}
{incomingSupplyOrders.length === 0 ? (
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
<div className="text-center">
<Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">
Пока нет заказов расходников
</h3>
<p className="text-white/60">
Здесь будут отображаться заказы расходников от селлеров
</p>
</div>
</Card>
) : (
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-white/20">
<th className="text-left p-4 text-white font-semibold">ID</th>
<th className="text-left p-4 text-white font-semibold">
Селлер
</th>
<th className="text-left p-4 text-white font-semibold">
Поставщик
</th>
<th className="text-left p-4 text-white font-semibold">
Дата поставки
</th>
<th className="text-left p-4 text-white font-semibold">
Дата заказа
</th>
<th className="text-left p-4 text-white font-semibold">
Количество
</th>
<th className="text-left p-4 text-white font-semibold">
Сумма
</th>
<th className="text-left p-4 text-white font-semibold">
Статус
</th>
</tr>
</thead>
<tbody>
{incomingSupplyOrders.map((order) => {
const isOrderExpanded = expandedOrders.has(order.id);
return (
<React.Fragment key={order.id}>
{/* Основная строка заказа */}
<tr
className="border-b border-white/10 hover:bg-white/5 transition-colors cursor-pointer"
onClick={() => toggleOrderExpansion(order.id)}
>
<td className="p-4">
<div className="flex items-center space-x-2">
{isOrderExpanded ? (
<ChevronDown className="h-4 w-4 text-white/60" />
) : (
<ChevronRight className="h-4 w-4 text-white/60" />
)}
<span className="text-white font-medium">
{order.id.slice(-8)}
</span>
</div>
</td>
<td className="p-4">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<User className="h-4 w-4 text-white/40" />
<span className="text-white font-medium">
{order.organization.name || order.organization.fullName || "Селлер"}
</span>
</div>
<p className="text-white/60 text-sm">
Тип: {order.organization.type}
</p>
</div>
</td>
<td className="p-4">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Building2 className="h-4 w-4 text-white/40" />
<span className="text-white font-medium">
{order.partner.name || order.partner.fullName || "Поставщик"}
</span>
</div>
<p className="text-white/60 text-sm">
ИНН: {order.partner.inn}
</p>
</div>
</td>
<td className="p-4">
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-white/40" />
<span className="text-white font-semibold">
{formatDate(order.deliveryDate)}
</span>
</div>
</td>
<td className="p-4">
<span className="text-white/80">
{formatDateTime(order.createdAt)}
</span>
</td>
<td className="p-4">
<span className="text-white font-semibold">
{order.totalItems} шт
</span>
</td>
<td className="p-4">
<div className="flex items-center space-x-2">
<DollarSign className="h-4 w-4 text-white/40" />
<span className="text-green-400 font-bold">
{formatCurrency(order.totalAmount)}
</span>
</div>
</td>
<td className="p-4">{getStatusBadge(order.status)}</td>
</tr>
{/* Развернутая информация о заказе */}
{isOrderExpanded && (
<tr>
<td colSpan={8} className="p-0">
<div className="bg-white/5 border-t border-white/10">
<div className="p-6">
<h4 className="text-white font-semibold mb-4">
Состав заказа от селлера:
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{order.items.map((item) => (
<Card
key={item.id}
className="bg-white/10 backdrop-blur border-white/20 p-4"
>
<div className="space-y-3">
<div>
<h5 className="text-white font-medium mb-1">
{item.product.name}
</h5>
<p className="text-white/60 text-sm">
Артикул: {item.product.article}
</p>
{item.product.category && (
<Badge className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs mt-2">
{item.product.category.name}
</Badge>
)}
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<p className="text-white/60">
Количество: {item.quantity} шт
</p>
<p className="text-white/60">
Цена: {formatCurrency(item.price)}
</p>
</div>
<div className="text-right">
<p className="text-green-400 font-semibold">
{formatCurrency(item.totalPrice)}
</p>
</div>
</div>
</div>
</Card>
))}
</div>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
</Card>
)}
</div>
);
}

View File

@ -7,7 +7,7 @@ import { Package, Wrench, RotateCcw, Building2 } from "lucide-react";
// Импорты компонентов подкатегорий
import { FulfillmentGoodsTab } from "./fulfillment-goods-tab";
import { PvzReturnsTab } from "./pvz-returns-tab";
import { SuppliesConsumablesTab } from "../../supplies/consumables-supplies/consumables-supplies-tab";
import { FulfillmentConsumablesOrdersTab } from "./fulfillment-consumables-orders-tab";
// Новые компоненты для детального просмотра (копия из supplies модуля)
import { FulfillmentDetailedSuppliesTab } from "./fulfillment-detailed-supplies-tab";
@ -68,7 +68,7 @@ export function FulfillmentSuppliesTab() {
<TabsContent value="consumables" className="flex-1 overflow-hidden">
<div className="h-full p-4 overflow-y-auto">
<SuppliesConsumablesTab />
<FulfillmentConsumablesOrdersTab />
</div>
</TabsContent>

View File

@ -313,7 +313,7 @@ export function FulfillmentGoodsTab() {
};
return (
<div className="space-y-6">
<div className="h-full flex flex-col space-y-6">
{/* Статистика товаров ФФ */}
<StatsGrid>
<StatsCard
@ -369,8 +369,8 @@ export function FulfillmentGoodsTab() {
</StatsGrid>
{/* Таблица поставок товаров ФФ */}
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-x-auto">
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden flex-1 flex flex-col">
<div className="overflow-auto flex-1">
<table className="w-full">
<thead>
<tr className="border-b border-white/20">

View File

@ -10,11 +10,11 @@ export function FulfillmentSuppliesTab() {
const [activeSubTab, setActiveSubTab] = useState("goods");
return (
<div className="h-full">
<div className="h-full overflow-hidden">
<Tabs
value={activeSubTab}
onValueChange={setActiveSubTab}
className="w-full h-full flex flex-col"
className="w-full h-full flex flex-col overflow-hidden"
>
{/* Подвкладки для ФФ */}
<TabsList className="grid grid-cols-3 bg-white/5 backdrop-blur border-white/10 mb-4 w-fit">
@ -38,15 +38,15 @@ export function FulfillmentSuppliesTab() {
</TabsTrigger>
</TabsList>
<TabsContent value="goods" className="mt-0 flex-1">
<TabsContent value="goods" className="mt-0 flex-1 overflow-hidden">
<FulfillmentGoodsTab />
</TabsContent>
<TabsContent value="supplies" className="mt-0 flex-1">
<TabsContent value="supplies" className="mt-0 flex-1 overflow-hidden">
<RealSupplyOrdersTab />
</TabsContent>
<TabsContent value="returns" className="mt-0 flex-1">
<TabsContent value="returns" className="mt-0 flex-1 overflow-hidden">
<PvzReturnsTab />
</TabsContent>
</Tabs>

View File

@ -318,7 +318,7 @@ export function PvzReturnsTab() {
};
return (
<div className="space-y-6">
<div className="h-full flex flex-col space-y-6">
{/* Статистика возвратов с ПВЗ */}
<StatsGrid>
<StatsCard
@ -369,8 +369,8 @@ export function PvzReturnsTab() {
</StatsGrid>
{/* Таблица возвратов с ПВЗ */}
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-x-auto">
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden flex-1 flex flex-col">
<div className="overflow-auto flex-1">
<table className="w-full">
<thead>
<tr className="border-b border-white/20">

View File

@ -165,7 +165,7 @@ export function RealSupplyOrdersTab() {
}
return (
<div className="space-y-6">
<div className="h-full flex flex-col space-y-6">
{/* Статистика заказов расходников */}
<StatsGrid>
<StatsCard
@ -219,8 +219,8 @@ export function RealSupplyOrdersTab() {
</div>
</Card>
) : (
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-x-auto">
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden flex-1 flex flex-col">
<div className="overflow-auto flex-1">
<table className="w-full">
<thead>
<tr className="border-b border-white/20">

View File

@ -82,11 +82,11 @@ export function SuppliesDashboard() {
</DropdownMenu>
</div>
<TabsContent value="fulfillment" className="mt-0 flex-1">
<TabsContent value="fulfillment" className="mt-0 flex-1 overflow-hidden">
<FulfillmentSuppliesTab />
</TabsContent>
<TabsContent value="marketplace" className="mt-0 flex-1">
<TabsContent value="marketplace" className="mt-0 flex-1 overflow-hidden">
<MarketplaceSuppliesTab />
</TabsContent>
</Tabs>

View File

@ -763,6 +763,7 @@ export const GET_SUPPLY_ORDERS = gql`
status
totalAmount
totalItems
fulfillmentCenterId
createdAt
updatedAt
partner {
@ -780,6 +781,12 @@ export const GET_SUPPLY_ORDERS = gql`
fullName
type
}
fulfillmentCenter {
id
name
fullName
type
}
items {
id
quantity

View File

@ -684,12 +684,13 @@ export const resolvers = {
throw new GraphQLError("У пользователя нет организации");
}
// Возвращаем заказы где текущая организация является заказчиком или поставщиком
// Возвращаем заказы где текущая организация является заказчиком, поставщиком или получателем
return await prisma.supplyOrder.findMany({
where: {
OR: [
{ organizationId: currentUser.organization.id }, // Заказы созданные организацией
{ partnerId: currentUser.organization.id }, // Заказы где организация - поставщик
{ fulfillmentCenterId: currentUser.organization.id }, // Заказы где организация - получатель (фулфилмент)
],
},
include: {
@ -703,6 +704,11 @@ export const resolvers = {
users: true,
},
},
fulfillmentCenter: {
include: {
users: true,
},
},
items: {
include: {
product: {
@ -3213,33 +3219,39 @@ export const resolvers = {
totalAmount: new Prisma.Decimal(totalAmount),
totalItems: totalItems,
organizationId: currentUser.organization.id,
fulfillmentCenterId: fulfillmentCenterId,
status: initialStatus as any,
items: {
create: orderItems,
},
},
include: {
partner: {
include: {
users: true,
},
include: {
partner: {
include: {
users: true,
},
organization: {
include: {
users: true,
},
},
organization: {
include: {
users: true,
},
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
fulfillmentCenter: {
include: {
users: true,
},
},
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
},
});
// Создаем расходники на основе заказанных товаров

View File

@ -519,6 +519,8 @@ export const typeDefs = gql`
status: SupplyOrderStatus!
totalAmount: Float!
totalItems: Int!
fulfillmentCenterId: ID
fulfillmentCenter: Organization
items: [SupplyOrderItem!]!
createdAt: DateTime!
updatedAt: DateTime!