This commit is contained in:
Bivekich
2025-07-28 14:38:36 +03:00
4 changed files with 819 additions and 390 deletions

View File

@ -0,0 +1,41 @@
"use client";
import React from "react";
import { Card } from "@/components/ui/card";
import { FulfillmentGoodsTab } from "./fulfillment-goods-tab";
import { RealSupplyOrdersTab } from "./real-supply-orders-tab";
import { SellerSupplyOrdersTab } from "./seller-supply-orders-tab";
import { useAuth } from "@/hooks/useAuth";
interface AllSuppliesTabProps {
pendingSupplyOrders?: number;
}
export function AllSuppliesTab({
pendingSupplyOrders = 0,
}: AllSuppliesTabProps) {
const { user } = useAuth();
// Определяем тип организации для выбора правильного компонента
const isWholesale = user?.organization?.type === "WHOLESALE";
return (
<div className="h-full overflow-hidden space-y-4">
{/* Секция товаров */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<h3 className="text-white font-semibold text-lg mb-3">Товары</h3>
<div className="h-64 overflow-hidden">
<FulfillmentGoodsTab />
</div>
</Card>
{/* Секция расходников */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<h3 className="text-white font-semibold text-lg mb-3">Расходники</h3>
<div className="h-64 overflow-hidden">
{isWholesale ? <RealSupplyOrdersTab /> : <SellerSupplyOrdersTab />}
</div>
</Card>
</div>
);
}

View File

@ -5,7 +5,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { FulfillmentGoodsTab } from "./fulfillment-goods-tab"; import { FulfillmentGoodsTab } from "./fulfillment-goods-tab";
import { RealSupplyOrdersTab } from "./real-supply-orders-tab"; import { RealSupplyOrdersTab } from "./real-supply-orders-tab";
import { SellerSupplyOrdersTab } from "./seller-supply-orders-tab"; import { SellerSupplyOrdersTab } from "./seller-supply-orders-tab";
import { PvzReturnsTab } from "./pvz-returns-tab"; import { AllSuppliesTab } from "./all-supplies-tab";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
interface FulfillmentSuppliesTabProps { interface FulfillmentSuppliesTabProps {
@ -17,7 +17,7 @@ export function FulfillmentSuppliesTab({
defaultSubTab, defaultSubTab,
pendingSupplyOrders = 0, pendingSupplyOrders = 0,
}: FulfillmentSuppliesTabProps) { }: FulfillmentSuppliesTabProps) {
const [activeSubTab, setActiveSubTab] = useState("goods"); const [activeSubTab, setActiveSubTab] = useState("all");
const { user } = useAuth(); const { user } = useAuth();
// Устанавливаем активную подвкладку при получении defaultSubTab // Устанавливаем активную подвкладку при получении defaultSubTab
@ -39,6 +39,12 @@ export function FulfillmentSuppliesTab({
> >
{/* Подвкладки для ФФ */} {/* Подвкладки для ФФ */}
<TabsList className="grid grid-cols-3 bg-white/5 backdrop-blur border-white/10 mb-2 w-fit text-sm"> <TabsList className="grid grid-cols-3 bg-white/5 backdrop-blur border-white/10 mb-2 w-fit text-sm">
<TabsTrigger
value="all"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/60 px-3 sm:px-4"
>
Все
</TabsTrigger>
<TabsTrigger <TabsTrigger
value="goods" value="goods"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/60 px-3 sm:px-4" className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/60 px-3 sm:px-4"
@ -47,7 +53,9 @@ export function FulfillmentSuppliesTab({
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="supplies" value="supplies"
className={`data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/60 px-3 sm:px-4 relative ${pendingSupplyOrders > 0 ? 'animate-pulse' : ''}`} className={`data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/60 px-3 sm:px-4 relative ${
pendingSupplyOrders > 0 ? "animate-pulse" : ""
}`}
> >
Расходники Расходники
{pendingSupplyOrders > 0 && ( {pendingSupplyOrders > 0 && (
@ -56,15 +64,12 @@ export function FulfillmentSuppliesTab({
</div> </div>
)} )}
</TabsTrigger> </TabsTrigger>
<TabsTrigger
value="returns"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/60 px-3 sm:px-4"
>
<span className="hidden sm:inline">Возвраты с ПВЗ</span>
<span className="sm:hidden">Возвраты</span>
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="all" className="mt-0 flex-1 overflow-hidden">
<AllSuppliesTab pendingSupplyOrders={pendingSupplyOrders} />
</TabsContent>
<TabsContent value="goods" className="mt-0 flex-1 overflow-hidden"> <TabsContent value="goods" className="mt-0 flex-1 overflow-hidden">
<FulfillmentGoodsTab /> <FulfillmentGoodsTab />
</TabsContent> </TabsContent>
@ -72,10 +77,6 @@ export function FulfillmentSuppliesTab({
<TabsContent value="supplies" className="mt-0 flex-1 overflow-hidden"> <TabsContent value="supplies" className="mt-0 flex-1 overflow-hidden">
{isWholesale ? <RealSupplyOrdersTab /> : <SellerSupplyOrdersTab />} {isWholesale ? <RealSupplyOrdersTab /> : <SellerSupplyOrdersTab />}
</TabsContent> </TabsContent>
<TabsContent value="returns" className="mt-0 flex-1 overflow-hidden">
<PvzReturnsTab />
</TabsContent>
</Tabs> </Tabs>
</div> </div>
); );

View File

@ -1,32 +1,36 @@
"use client"; "use client";
import React, { 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";
import { StatsCard } from "../ui/stats-card";
import { StatsGrid } from "../ui/stats-grid";
import { useQuery, useMutation } from "@apollo/client"; import { useQuery, useMutation } from "@apollo/client";
import { Card } from "@/components/ui/card";
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 { toast } from "sonner";
import { GET_SUPPLY_ORDERS } from "@/graphql/queries"; import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations"; import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { import {
ChevronRight,
ChevronDown,
CheckCircle,
XCircle,
Truck,
Calendar, Calendar,
Building2, User,
TrendingUp,
DollarSign, DollarSign,
Wrench, Wrench,
Package2, Package2,
ChevronDown,
ChevronRight,
User,
CheckCircle,
XCircle,
Clock, Clock,
Truck, TrendingUp,
TrendingDown,
Search,
Store,
ArrowUpDown,
} from "lucide-react"; } from "lucide-react";
// Типы для данных заказов
interface SupplyOrderItem { interface SupplyOrderItem {
id: string; id: string;
quantity: number; quantity: number;
@ -36,7 +40,6 @@ interface SupplyOrderItem {
id: string; id: string;
name: string; name: string;
article: string; article: string;
description?: string;
category?: { category?: {
id: string; id: string;
name: string; name: string;
@ -46,20 +49,15 @@ interface SupplyOrderItem {
interface SupplyOrder { interface SupplyOrder {
id: string; id: string;
organizationId: string; status: string;
deliveryDate: string;
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
totalAmount: number; totalAmount: number;
totalItems: number; totalItems: number;
fulfillmentCenterId?: string; deliveryDate: string;
createdAt: string; createdAt: string;
updatedAt: string;
partner: { partner: {
id: string; id: string;
name?: string; name?: string;
fullName?: string; fullName?: string;
inn: string;
address?: string;
phones?: string[]; phones?: string[];
emails?: string[]; emails?: string[];
}; };
@ -78,8 +76,96 @@ interface SupplyOrder {
items: SupplyOrderItem[]; items: SupplyOrderItem[];
} }
// Компонент для заголовка таблицы
const TableHeader = ({
children,
field,
sortable = false,
sortField,
sortOrder,
onSort,
}: {
children: React.ReactNode;
field: string;
sortable?: boolean;
sortField?: string;
sortOrder?: "asc" | "desc";
onSort?: (field: string) => void;
}) => (
<div
className={`px-3 py-2 text-xs font-bold text-white flex items-center justify-between ${
sortable ? "cursor-pointer hover:bg-white/5" : ""
}`}
onClick={() => sortable && onSort && onSort(field)}
>
<span>{children}</span>
{sortable && (
<ArrowUpDown
className={`h-3 w-3 ml-1 ${
sortField === field ? "text-blue-400" : "text-white/40"
}`}
/>
)}
</div>
);
// Компонент для статистических карточек
const StatsCard = ({
title,
value,
change = 0,
icon: Icon,
iconColor = "text-blue-400",
iconBg = "bg-blue-500/20",
subtitle,
}: {
title: string;
value: string | number;
change?: number;
icon: React.ComponentType<any>;
iconColor?: string;
iconBg?: string;
subtitle?: string;
}) => (
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className={`p-2 rounded-lg ${iconBg}`}>
<Icon className={`h-5 w-5 ${iconColor}`} />
</div>
<div>
<p className="text-white/60 text-sm">{title}</p>
<div className="flex items-center space-x-2">
<p className="text-white text-xl font-bold">{value}</p>
{change !== 0 && (
<div className="flex items-center space-x-1">
{change > 0 ? (
<TrendingUp className="h-3 w-3 text-green-400" />
) : (
<TrendingDown className="h-3 w-3 text-red-400" />
)}
<span
className={`text-xs font-medium ${
change > 0 ? "text-green-400" : "text-red-400"
}`}
>
{Math.abs(change)}%
</span>
</div>
)}
</div>
{subtitle && <p className="text-white/40 text-xs mt-1">{subtitle}</p>}
</div>
</div>
</div>
</Card>
);
export function RealSupplyOrdersTab() { export function RealSupplyOrdersTab() {
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set()); const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
const [searchTerm, setSearchTerm] = useState("");
const [sortField, setSortField] = useState<string>("createdAt");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
const { user } = useAuth(); const { user } = useAuth();
const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, { const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, {
@ -94,7 +180,7 @@ export function RealSupplyOrdersTab() {
onCompleted: (data) => { onCompleted: (data) => {
if (data.updateSupplyOrderStatus.success) { if (data.updateSupplyOrderStatus.success) {
toast.success(data.updateSupplyOrderStatus.message); toast.success(data.updateSupplyOrderStatus.message);
refetch(); // Обновляем список заказов refetch();
} else { } else {
toast.error(data.updateSupplyOrderStatus.message); toast.error(data.updateSupplyOrderStatus.message);
} }
@ -109,14 +195,14 @@ export function RealSupplyOrdersTab() {
// Получаем ID текущей организации (поставщика) // Получаем ID текущей организации (поставщика)
const currentOrganizationId = user?.organization?.id; const currentOrganizationId = user?.organization?.id;
// Фильтруем заказы где текущая организация является поставщиком (заказы К поставщику) // Фильтруем заказы где текущая организация является поставщиком
const incomingSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( const incomingSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter(
(order: SupplyOrder) => { (order: SupplyOrder) => {
// Показываем заказы где текущий поставщик указан как поставщик (partnerId)
return order.partner.id === currentOrganizationId; return order.partner.id === currentOrganizationId;
} }
); );
// Функции для работы с таблицей
const toggleOrderExpansion = (orderId: string) => { const toggleOrderExpansion = (orderId: string) => {
const newExpanded = new Set(expandedOrders); const newExpanded = new Set(expandedOrders);
if (newExpanded.has(orderId)) { if (newExpanded.has(orderId)) {
@ -127,66 +213,85 @@ export function RealSupplyOrdersTab() {
setExpandedOrders(newExpanded); setExpandedOrders(newExpanded);
}; };
const handleStatusUpdate = async ( const handleSort = (field: string) => {
orderId: string, if (sortField === field) {
newStatus: SupplyOrder["status"] setSortOrder(sortOrder === "asc" ? "desc" : "asc");
) => { } else {
setSortField(field);
setSortOrder("asc");
}
};
const handleStatusUpdate = async (orderId: string, status: string) => {
try { try {
await updateSupplyOrderStatus({ await updateSupplyOrderStatus({
variables: { variables: {
id: orderId, id: orderId,
status: newStatus, status,
}, },
}); });
} catch (error) { } catch (error) {
console.error("Error updating status:", error); console.error("Error updating order status:", error);
} }
}; };
const getStatusBadge = (status: SupplyOrder["status"]) => { // Фильтрация и сортировка заказов
const statusMap = { const filteredAndSortedOrders = incomingSupplyOrders
PENDING: { .filter((order) => {
label: "Ожидает одобрения", const searchLower = searchTerm.toLowerCase();
color: "bg-blue-500/20 text-blue-300 border-blue-500/30", return (
icon: Clock, order.id.toLowerCase().includes(searchLower) ||
}, (order.organization.name || order.organization.fullName || "")
CONFIRMED: { .toLowerCase()
label: "Одобрена", .includes(searchLower) ||
color: "bg-green-500/20 text-green-300 border-green-500/30", order.items.some(
icon: CheckCircle, (item) =>
}, item.product.name.toLowerCase().includes(searchLower) ||
IN_TRANSIT: { item.product.article.toLowerCase().includes(searchLower)
label: "В пути", )
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", );
icon: Truck, })
}, .sort((a, b) => {
DELIVERED: { let aValue, bValue;
label: "Доставлена",
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
icon: Package2,
},
CANCELLED: {
label: "Отклонена",
color: "bg-red-500/20 text-red-300 border-red-500/30",
icon: XCircle,
},
};
const { label, color, icon: Icon } = statusMap[status]; switch (sortField) {
case "organization":
aValue = a.organization.name || a.organization.fullName || "";
bValue = b.organization.name || b.organization.fullName || "";
break;
case "totalAmount":
aValue = a.totalAmount;
bValue = b.totalAmount;
break;
case "totalItems":
aValue = a.totalItems;
bValue = b.totalItems;
break;
case "deliveryDate":
aValue = new Date(a.deliveryDate).getTime();
bValue = new Date(b.deliveryDate).getTime();
break;
case "status":
aValue = a.status;
bValue = b.status;
break;
default:
aValue = new Date(a.createdAt).getTime();
bValue = new Date(b.createdAt).getTime();
}
return ( if (aValue < bValue) return sortOrder === "asc" ? -1 : 1;
<Badge className={`${color} border flex items-center gap-1`}> if (aValue > bValue) return sortOrder === "asc" ? 1 : -1;
<Icon className="h-3 w-3" /> return 0;
{label} });
</Badge>
);
};
// Функции форматирования
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", { return new Intl.NumberFormat("ru-RU", {
style: "currency", style: "currency",
currency: "RUB", currency: "RUB",
minimumFractionDigits: 0, minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount); }).format(amount);
}; };
@ -208,7 +313,120 @@ export function RealSupplyOrdersTab() {
}); });
}; };
// Статистика для поставщика const formatNumber = (num: number) => {
return new Intl.NumberFormat("ru-RU").format(num);
};
const getStatusBadge = (status: string) => {
const statusConfig = {
PENDING: {
label: "Ожидает",
className: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
},
CONFIRMED: {
label: "Одобрена",
className: "bg-green-500/20 text-green-300 border-green-500/30",
},
IN_TRANSIT: {
label: "В пути",
className: "bg-blue-500/20 text-blue-300 border-blue-500/30",
},
DELIVERED: {
label: "Доставлена",
className: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30",
},
CANCELLED: {
label: "Отменена",
className: "bg-red-500/20 text-red-300 border-red-500/30",
},
};
const config = statusConfig[status as keyof typeof statusConfig] || {
label: status,
className: "bg-gray-500/20 text-gray-300 border-gray-500/30",
};
return (
<Badge className={`${config.className} border text-xs`}>
{config.label}
</Badge>
);
};
const getInitials = (name: string): string => {
return name
.split(" ")
.map((word) => word.charAt(0))
.join("")
.toUpperCase()
.slice(0, 2);
};
const getColorForOrder = (orderId: string): string => {
const colors = [
"bg-blue-500",
"bg-green-500",
"bg-purple-500",
"bg-orange-500",
"bg-pink-500",
"bg-indigo-500",
"bg-teal-500",
"bg-red-500",
];
const hash = orderId
.split("")
.reduce((acc, char) => acc + char.charCodeAt(0), 0);
return colors[hash % colors.length];
};
// Цветовые схемы для заказов
const getColorScheme = (orderId: string) => {
const colorSchemes = [
{
bg: "bg-blue-500/5",
border: "border-blue-500/30",
borderLeft: "border-l-blue-400",
text: "text-blue-100",
indicator: "bg-blue-400 border-blue-300",
hover: "hover:bg-blue-500/10",
header: "bg-blue-500/20 border-blue-500/40",
},
{
bg: "bg-pink-500/5",
border: "border-pink-500/30",
borderLeft: "border-l-pink-400",
text: "text-pink-100",
indicator: "bg-pink-400 border-pink-300",
hover: "hover:bg-pink-500/10",
header: "bg-pink-500/20 border-pink-500/40",
},
{
bg: "bg-emerald-500/5",
border: "border-emerald-500/30",
borderLeft: "border-l-emerald-400",
text: "text-emerald-100",
indicator: "bg-emerald-400 border-emerald-300",
hover: "hover:bg-emerald-500/10",
header: "bg-emerald-500/20 border-emerald-500/40",
},
{
bg: "bg-orange-500/5",
border: "border-orange-500/30",
borderLeft: "border-l-orange-400",
text: "text-orange-100",
indicator: "bg-orange-400 border-orange-300",
hover: "hover:bg-orange-500/10",
header: "bg-orange-500/20 border-orange-500/40",
},
];
const hash = orderId
.split("")
.reduce((acc, char) => acc + char.charCodeAt(0), 0);
return colorSchemes[hash % colorSchemes.length];
};
// Подсчет статистики
const totalOrders = incomingSupplyOrders.length; const totalOrders = incomingSupplyOrders.length;
const totalAmount = incomingSupplyOrders.reduce( const totalAmount = incomingSupplyOrders.reduce(
(sum, order) => sum + order.totalAmount, (sum, order) => sum + order.totalAmount,
@ -228,6 +446,22 @@ export function RealSupplyOrdersTab() {
(order) => order.status === "IN_TRANSIT" (order) => order.status === "IN_TRANSIT"
).length; ).length;
// Подсчет общих итогов для отображения в строке итогов
const totals = {
orders: filteredAndSortedOrders.length,
amount: filteredAndSortedOrders.reduce(
(sum, order) => sum + order.totalAmount,
0
),
items: filteredAndSortedOrders.reduce(
(sum, order) => sum + order.totalItems,
0
),
pending: filteredAndSortedOrders.filter(
(order) => order.status === "PENDING"
).length,
};
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
@ -250,295 +484,401 @@ export function RealSupplyOrdersTab() {
} }
return ( return (
<div className="space-y-6"> <div className="h-full flex flex-col overflow-hidden">
{/* Статистика входящих заявок */} {/* Статистические карточки - 30% экрана */}
<StatsGrid> <div className="flex-shrink-0 mb-4" style={{ maxHeight: "30vh" }}>
<StatsCard <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
title="Всего заявок" <StatsCard
value={totalOrders} title="Всего заявок"
icon={Package2} value={totalOrders}
iconColor="text-orange-400" icon={Package2}
iconBg="bg-orange-500/20" iconColor="text-orange-400"
subtitle="Заявки от селлеров" iconBg="bg-orange-500/20"
/> subtitle="Заявки от селлеров"
/>
<StatsCard
title="Ожидают одобрения"
value={pendingOrders}
icon={Clock}
iconColor="text-blue-400"
iconBg="bg-blue-500/20"
subtitle="Требуют решения"
/>
<StatsCard
title="Одобрено"
value={approvedOrders}
icon={CheckCircle}
iconColor="text-green-400"
iconBg="bg-green-500/20"
subtitle="Готовы к отправке"
/>
<StatsCard
title="В пути"
value={inTransitOrders}
icon={Truck}
iconColor="text-yellow-400"
iconBg="bg-yellow-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-purple-400"
iconBg="bg-purple-500/20"
subtitle="Количество товаров"
/>
</div>
</div>
<StatsCard {/* Основная таблица - 70% экрана */}
title="Ожидают одобрения" <div className="flex-1 flex flex-col overflow-hidden">
value={pendingOrders} <Card className="bg-white/10 backdrop-blur border-white/20 flex-1 flex flex-col overflow-hidden">
icon={Clock} {/* Шапка таблицы с поиском */}
iconColor="text-blue-400" <div className="p-4 border-b border-white/10 flex-shrink-0">
iconBg="bg-blue-500/20" <div className="flex items-center justify-between">
subtitle="Требуют решения" <h2 className="text-base font-semibold text-white flex items-center space-x-2">
/> <Store className="h-4 w-4 text-blue-400" />
<span>Заявки на расходники</span>
</h2>
<StatsCard {/* Поиск */}
title="Одобрено" <div className="relative mx-2.5 flex-1 max-w-xs">
value={approvedOrders} <Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-white/40" />
icon={CheckCircle} <Input
iconColor="text-green-400" placeholder="Поиск по заявкам..."
iconBg="bg-green-500/20" value={searchTerm}
subtitle="Готовы к отправке" onChange={(e) => setSearchTerm(e.target.value)}
/> className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40"
/>
</div>
<StatsCard <Badge
title="В пути" variant="secondary"
value={inTransitOrders} className="bg-blue-500/20 text-blue-300 text-xs"
icon={Truck} >
iconColor="text-yellow-400" {filteredAndSortedOrders.length} заявок
iconBg="bg-yellow-500/20" </Badge>
subtitle="Доставляются" </div>
/>
<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-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> </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}> <div className="flex-shrink-0 bg-blue-500/20 border-b border-blue-500/40">
{/* Основная строка заказа */} <div className="grid grid-cols-7 gap-0">
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors"> <TableHeader
<td className="p-4"> field="id"
<div className="flex items-center space-x-2"> sortable
<button sortField={sortField}
onClick={() => toggleOrderExpansion(order.id)} sortOrder={sortOrder}
className="text-white/60 hover:text-white" onSort={handleSort}
> >
{isOrderExpanded ? ( / ID
<ChevronDown className="h-4 w-4" /> </TableHeader>
) : ( <TableHeader
<ChevronRight className="h-4 w-4" /> field="organization"
)} sortable
</button> sortField={sortField}
<span className="text-white font-medium"> sortOrder={sortOrder}
{order.id.slice(-8)} onSort={handleSort}
</span> >
</div> Заказчик
</td> </TableHeader>
<td className="p-4"> <TableHeader
<div className="space-y-1"> field="deliveryDate"
<div className="flex items-center space-x-2"> sortable
<User className="h-4 w-4 text-white/40" /> sortField={sortField}
<span className="text-white font-medium"> sortOrder={sortOrder}
{order.organization.name || onSort={handleSort}
order.organization.fullName || >
"Заказчик"} Дата поставки
</span> </TableHeader>
</div> <TableHeader
<p className="text-white/60 text-sm"> field="totalItems"
Тип: {order.organization.type} sortable
</p> sortField={sortField}
</div> sortOrder={sortOrder}
</td> onSort={handleSort}
<td className="p-4"> >
<div className="flex items-center space-x-2"> Количество
<Calendar className="h-4 w-4 text-white/40" /> </TableHeader>
<span className="text-white font-semibold"> <TableHeader
{formatDate(order.deliveryDate)} field="totalAmount"
</span> sortable
</div> sortField={sortField}
</td> sortOrder={sortOrder}
<td className="p-4"> onSort={handleSort}
<span className="text-white/80"> >
{formatDateTime(order.createdAt)} Сумма
</TableHeader>
<TableHeader
field="status"
sortable
sortField={sortField}
sortOrder={sortOrder}
onSort={handleSort}
>
Статус
</TableHeader>
<TableHeader field="actions">Действия</TableHeader>
</div>
</div>
{/* Строка с итогами */}
<div className="flex-shrink-0 bg-blue-500/25 border-b border-blue-500/50">
<div className="grid grid-cols-7 gap-0">
<div className="px-3 py-2 text-xs font-bold text-blue-300">
ИТОГО ({totals.orders})
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{totals.orders} заказчиков
</div>
<div className="px-3 py-2 text-xs font-bold text-white">-</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{formatNumber(totals.items)} шт
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{formatCurrency(totals.amount)}
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{totals.pending} ожидают
</div>
<div className="px-3 py-2 text-xs font-bold text-white">-</div>
</div>
</div>
{/* Скроллируемый контент таблицы */}
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
{filteredAndSortedOrders.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Wrench className="h-12 w-12 text-white/40 mx-auto mb-4" />
<p className="text-white/60 font-medium">
{incomingSupplyOrders.length === 0
? "Нет заявок на расходники"
: "Заявки не найдены"}
</p>
<p className="text-white/40 text-sm mt-2">
{incomingSupplyOrders.length === 0
? "Здесь будут отображаться заявки от селлеров"
: searchTerm
? "Попробуйте изменить поисковый запрос"
: "Данные о заявках будут отображены здесь"}
</p>
</div>
</div>
) : (
filteredAndSortedOrders.map((order, index) => {
const colorScheme = getColorScheme(order.id);
const isOrderExpanded = expandedOrders.has(order.id);
const organizationName =
order.organization.name ||
order.organization.fullName ||
"Заказчик";
return (
<div
key={order.id}
className={`border-b ${colorScheme.border} ${colorScheme.hover} transition-colors border-l-8 ${colorScheme.borderLeft} ${colorScheme.bg} shadow-sm hover:shadow-md`}
>
{/* Основная строка заказа */}
<div className="grid grid-cols-7 gap-0">
<div className="px-3 py-2.5 flex items-center space-x-2">
<span className="text-white/60 text-xs">
{filteredAndSortedOrders.length - index}
</span>
<button
onClick={() => toggleOrderExpansion(order.id)}
className="text-white/60 hover:text-white"
>
{isOrderExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
<span className="text-white font-medium text-xs">
{order.id.slice(-8)}
</span>
</div>
<div className="px-3 py-2.5 flex items-center space-x-2">
<Avatar className="w-6 h-6">
<AvatarFallback
className={`${getColorForOrder(
order.id
)} text-white text-xs`}
>
{getInitials(organizationName)}
</AvatarFallback>
</Avatar>
<div>
<span className="text-white font-medium text-sm">
{organizationName}
</span> </span>
</td> <p className="text-white/60 text-xs">
<td className="p-4"> {order.organization.type}
<span className="text-white font-semibold"> </p>
{order.totalItems} шт </div>
</span> </div>
</td>
<td className="p-4"> <div className="px-3 py-2.5 flex items-center space-x-2">
<div className="flex items-center space-x-2"> <Calendar className="h-4 w-4 text-white/40" />
<DollarSign className="h-4 w-4 text-white/40" /> <span className="text-white font-semibold text-sm">
<span className="text-green-400 font-bold"> {formatDate(order.deliveryDate)}
{formatCurrency(order.totalAmount)} </span>
</span> </div>
</div>
</td> <div className="px-3 py-2.5">
<td className="p-4">{getStatusBadge(order.status)}</td> <span className="text-white font-semibold text-sm">
<td className="p-4"> {order.totalItems} шт
<div className="flex items-center space-x-2"> </span>
{order.status === "PENDING" && ( </div>
<>
<Button <div className="px-3 py-2.5 flex items-center space-x-2">
size="sm" <DollarSign className="h-4 w-4 text-white/40" />
onClick={() => <span className="text-green-400 font-bold text-sm">
handleStatusUpdate(order.id, "CONFIRMED") {formatCurrency(order.totalAmount)}
} </span>
disabled={updating} </div>
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30"
> <div className="px-3 py-2.5">
<CheckCircle className="h-4 w-4 mr-1" /> {getStatusBadge(order.status)}
Одобрить </div>
</Button>
<Button <div className="px-3 py-2.5">
size="sm" <div className="flex items-center space-x-1">
onClick={() => {order.status === "PENDING" && (
handleStatusUpdate(order.id, "CANCELLED") <>
}
disabled={updating}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30"
>
<XCircle className="h-4 w-4 mr-1" />
Отказать
</Button>
</>
)}
{order.status === "CONFIRMED" && (
<Button <Button
size="sm" size="sm"
onClick={() => onClick={() =>
handleStatusUpdate(order.id, "IN_TRANSIT") handleStatusUpdate(order.id, "CONFIRMED")
} }
disabled={updating} disabled={updating}
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border border-yellow-500/30" className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-2 py-1 h-6"
> >
<Truck className="h-4 w-4 mr-1" /> <CheckCircle className="h-3 w-3 mr-1" />
Отправить Одобрить
</Button> </Button>
)} <Button
{order.status === "CANCELLED" && ( size="sm"
<span className="text-red-400 text-sm"> onClick={() =>
Отклонена handleStatusUpdate(order.id, "CANCELLED")
</span> }
)} disabled={updating}
{order.status === "IN_TRANSIT" && ( className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30 text-xs px-2 py-1 h-6"
<span className="text-yellow-400 text-sm"> >
В пути <XCircle className="h-3 w-3 mr-1" />
</span> Отклонить
)} </Button>
{order.status === "DELIVERED" && ( </>
<span className="text-green-400 text-sm"> )}
Доставлена {order.status === "CONFIRMED" && (
</span> <Button
)} size="sm"
</div> onClick={() =>
</td> handleStatusUpdate(order.id, "IN_TRANSIT")
</tr> }
disabled={updating}
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border border-yellow-500/30 text-xs px-2 py-1 h-6"
>
<Truck className="h-3 w-3 mr-1" />
Отправить
</Button>
)}
{order.status === "CANCELLED" && (
<span className="text-red-400 text-xs">
Отклонена
</span>
)}
{order.status === "IN_TRANSIT" && (
<span className="text-yellow-400 text-xs">
В пути
</span>
)}
{order.status === "DELIVERED" && (
<span className="text-green-400 text-xs">
Доставлена
</span>
)}
</div>
</div>
</div>
{/* Развернутая информация о заказе */} {/* Развернутая информация о заказе */}
{isOrderExpanded && ( {isOrderExpanded && (
<tr> <div
<td colSpan={8} className="p-0"> className={`${colorScheme.bg} border-t ${colorScheme.border}`}
<div className="bg-white/5 border-t border-white/10"> >
<div className="p-6"> <div className="p-6">
<h4 className="text-white font-semibold mb-4"> <div className="flex items-center justify-between mb-4">
Состав заявки: <h4 className="text-white font-semibold">
</h4> Состав заявки:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> </h4>
{order.items.map((item) => ( <div className="flex items-center space-x-2 text-white/60 text-sm">
<Card <Calendar className="h-4 w-4" />
key={item.id} <span>
className="bg-white/10 backdrop-blur border-white/20 p-4" Дата создания: {formatDateTime(order.createdAt)}
> </span>
<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> </div>
</td> </div>
</tr> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
)} {order.items.map((item) => (
</React.Fragment> <Card
); key={item.id}
})} className="bg-white/10 backdrop-blur border-white/20 p-4"
</tbody> >
</table> <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>
)}
</div>
);
})
)}
</div> </div>
</Card> </Card>
)} </div>
</div> </div>
); );
} }

View File

@ -8,10 +8,19 @@ import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar"; import { useSidebar } from "@/hooks/useSidebar";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Plus, Package, Wrench, ChevronDown, AlertTriangle } from "lucide-react"; import {
Plus,
Package,
Wrench,
ChevronDown,
AlertTriangle,
} from "lucide-react";
import { GET_PENDING_SUPPLIES_COUNT } from "@/graphql/queries"; import { GET_PENDING_SUPPLIES_COUNT } from "@/graphql/queries";
import { FulfillmentSuppliesTab } from "./fulfillment-supplies/fulfillment-supplies-tab"; import { FulfillmentGoodsTab } from "./fulfillment-supplies/fulfillment-goods-tab";
import { MarketplaceSuppliesTab } from "./marketplace-supplies/marketplace-supplies-tab"; import { RealSupplyOrdersTab } from "./fulfillment-supplies/real-supply-orders-tab";
import { SellerSupplyOrdersTab } from "./fulfillment-supplies/seller-supply-orders-tab";
import { AllSuppliesTab } from "./fulfillment-supplies/all-supplies-tab";
import { useAuth } from "@/hooks/useAuth";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -22,13 +31,14 @@ import {
export function SuppliesDashboard() { export function SuppliesDashboard() {
const { getSidebarMargin } = useSidebar(); const { getSidebarMargin } = useSidebar();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [activeTab, setActiveTab] = useState("fulfillment"); const [activeTab, setActiveTab] = useState("all");
const { user } = useAuth();
// Загружаем счетчик поставок, требующих одобрения // Загружаем счетчик поставок, требующих одобрения
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, { const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
pollInterval: 30000, // Обновляем каждые 30 секунд pollInterval: 30000, // Обновляем каждые 30 секунд
fetchPolicy: 'cache-first', fetchPolicy: "cache-first",
errorPolicy: 'ignore', errorPolicy: "ignore",
}); });
const pendingCount = pendingData?.pendingSuppliesCount; const pendingCount = pendingData?.pendingSuppliesCount;
@ -38,10 +48,15 @@ export function SuppliesDashboard() {
useEffect(() => { useEffect(() => {
const tab = searchParams.get("tab"); const tab = searchParams.get("tab");
if (tab === "consumables") { if (tab === "consumables") {
setActiveTab("fulfillment"); // Устанавливаем основную вкладку "Поставки на ФФ" setActiveTab("supplies");
} else if (tab === "goods") {
setActiveTab("goods");
} }
}, [searchParams]); }, [searchParams]);
// Определяем тип организации для выбора правильного компонента
const isWholesale = user?.organization?.type === "WHOLESALE";
return ( return (
<div className="h-screen flex overflow-hidden"> <div className="h-screen flex overflow-hidden">
<Sidebar /> <Sidebar />
@ -54,41 +69,73 @@ export function SuppliesDashboard() {
<Alert className="mb-4 bg-blue-500/20 border-blue-400/30 text-blue-300 animate-pulse"> <Alert className="mb-4 bg-blue-500/20 border-blue-400/30 text-blue-300 animate-pulse">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<AlertDescription> <AlertDescription>
У вас есть {pendingCount.total} элемент{pendingCount.total > 1 ? (pendingCount.total < 5 ? 'а' : 'ов') : ''}, требующ{pendingCount.total > 1 ? 'их' : 'ий'} одобрения: У вас есть {pendingCount.total} элемент
{pendingCount.supplyOrders > 0 && ` ${pendingCount.supplyOrders} заказ${pendingCount.supplyOrders > 1 ? (pendingCount.supplyOrders < 5 ? 'а' : 'ов') : ''} поставок`} {pendingCount.total > 1
{pendingCount.incomingRequests > 0 && pendingCount.supplyOrders > 0 && ', '} ? pendingCount.total < 5
{pendingCount.incomingRequests > 0 && ` ${pendingCount.incomingRequests} заявк${pendingCount.incomingRequests > 1 ? (pendingCount.incomingRequests < 5 ? 'и' : '') : 'а'} на партнерство`} ? "а"
: "ов"
: ""}
, требующ{pendingCount.total > 1 ? "их" : "ий"} одобрения:
{pendingCount.supplyOrders > 0 &&
` ${pendingCount.supplyOrders} заказ${
pendingCount.supplyOrders > 1
? pendingCount.supplyOrders < 5
? "а"
: "ов"
: ""
} поставок`}
{pendingCount.incomingRequests > 0 &&
pendingCount.supplyOrders > 0 &&
", "}
{pendingCount.incomingRequests > 0 &&
` ${pendingCount.incomingRequests} заявк${
pendingCount.incomingRequests > 1
? pendingCount.incomingRequests < 5
? "и"
: ""
: "а"
} на партнерство`}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
{/* Главные вкладки с кнопкой создания */} {/* Основные вкладки с кнопкой создания */}
<Tabs <Tabs
value={activeTab} value={activeTab}
onValueChange={setActiveTab} onValueChange={setActiveTab}
className="w-full h-full flex flex-col" className="w-full h-full flex flex-col"
> >
<div className="flex items-center justify-between mb-1 flex-wrap gap-2"> <div className="flex items-center justify-between mb-1 flex-wrap gap-2">
<TabsList className={`grid grid-cols-2 bg-white/10 backdrop-blur border-white/20 w-fit text-sm ${hasPendingItems ? 'ring-2 ring-blue-400/50' : ''}`}> <TabsList
className={`grid grid-cols-3 bg-white/10 backdrop-blur border-white/20 w-fit text-sm ${
hasPendingItems ? "ring-2 ring-blue-400/50" : ""
}`}
>
<TabsTrigger <TabsTrigger
value="fulfillment" value="all"
className={`data-[state=active]:bg-gradient-to-r data-[state=active]:from-purple-500 data-[state=active]:to-pink-500 data-[state=active]:text-white text-white/60 px-3 sm:px-6 relative ${pendingCount?.supplyOrders > 0 ? 'animate-pulse' : ''}`} className="data-[state=active]:bg-gradient-to-r data-[state=active]:from-purple-500 data-[state=active]:to-pink-500 data-[state=active]:text-white text-white/60 px-3 sm:px-6"
> >
<span className="hidden sm:inline">Поставки на ФФ</span> Все
<span className="sm:hidden">ФФ</span> </TabsTrigger>
<TabsTrigger
value="goods"
className="data-[state=active]:bg-gradient-to-r data-[state=active]:from-purple-500 data-[state=active]:to-pink-500 data-[state=active]:text-white text-white/60 px-3 sm:px-6"
>
Товар
</TabsTrigger>
<TabsTrigger
value="supplies"
className={`data-[state=active]:bg-gradient-to-r data-[state=active]:from-purple-500 data-[state=active]:to-pink-500 data-[state=active]:text-white text-white/60 px-3 sm:px-6 relative ${
pendingCount?.supplyOrders > 0 ? "animate-pulse" : ""
}`}
>
Расходники
{pendingCount?.supplyOrders > 0 && ( {pendingCount?.supplyOrders > 0 && (
<div className="absolute -top-1 -right-1 w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center text-xs font-bold text-white"> <div className="absolute -top-1 -right-1 w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center text-xs font-bold text-white">
{pendingCount.supplyOrders} {pendingCount.supplyOrders}
</div> </div>
)} )}
</TabsTrigger> </TabsTrigger>
<TabsTrigger
value="marketplace"
className="data-[state=active]:bg-gradient-to-r data-[state=active]:from-purple-500 data-[state=active]:to-pink-500 data-[state=active]:text-white text-white/60 px-3 sm:px-6"
>
<span className="hidden sm:inline">Поставки на Маркетплейсы</span>
<span className="sm:hidden">МП</span>
</TabsTrigger>
</TabsList> </TabsList>
<DropdownMenu> <DropdownMenu>
@ -129,25 +176,25 @@ export function SuppliesDashboard() {
</DropdownMenu> </DropdownMenu>
</div> </div>
<TabsContent <TabsContent value="all" className="mt-0 flex-1 overflow-hidden">
value="fulfillment" <AllSuppliesTab
className="mt-0 flex-1 overflow-hidden"
>
<FulfillmentSuppliesTab
defaultSubTab={
searchParams.get("tab") === "consumables"
? "supplies"
: undefined
}
pendingSupplyOrders={pendingCount?.supplyOrders || 0} pendingSupplyOrders={pendingCount?.supplyOrders || 0}
/> />
</TabsContent> </TabsContent>
<TabsContent value="goods" className="mt-0 flex-1 overflow-hidden">
<FulfillmentGoodsTab />
</TabsContent>
<TabsContent <TabsContent
value="marketplace" value="supplies"
className="mt-0 flex-1 overflow-hidden" className="mt-0 flex-1 overflow-hidden"
> >
<MarketplaceSuppliesTab /> {isWholesale ? (
<RealSupplyOrdersTab />
) : (
<SellerSupplyOrdersTab />
)}
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>