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 { RealSupplyOrdersTab } from "./real-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";
interface FulfillmentSuppliesTabProps {
@ -17,7 +17,7 @@ export function FulfillmentSuppliesTab({
defaultSubTab,
pendingSupplyOrders = 0,
}: FulfillmentSuppliesTabProps) {
const [activeSubTab, setActiveSubTab] = useState("goods");
const [activeSubTab, setActiveSubTab] = useState("all");
const { user } = useAuth();
// Устанавливаем активную подвкладку при получении 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">
<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
value="goods"
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
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 && (
@ -56,15 +64,12 @@ export function FulfillmentSuppliesTab({
</div>
)}
</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>
<TabsContent value="all" className="mt-0 flex-1 overflow-hidden">
<AllSuppliesTab pendingSupplyOrders={pendingSupplyOrders} />
</TabsContent>
<TabsContent value="goods" className="mt-0 flex-1 overflow-hidden">
<FulfillmentGoodsTab />
</TabsContent>
@ -72,10 +77,6 @@ export function FulfillmentSuppliesTab({
<TabsContent value="supplies" className="mt-0 flex-1 overflow-hidden">
{isWholesale ? <RealSupplyOrdersTab /> : <SellerSupplyOrdersTab />}
</TabsContent>
<TabsContent value="returns" className="mt-0 flex-1 overflow-hidden">
<PvzReturnsTab />
</TabsContent>
</Tabs>
</div>
);

View File

@ -1,32 +1,36 @@
"use client";
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 { 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 { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import {
ChevronRight,
ChevronDown,
CheckCircle,
XCircle,
Truck,
Calendar,
Building2,
TrendingUp,
User,
DollarSign,
Wrench,
Package2,
ChevronDown,
ChevronRight,
User,
CheckCircle,
XCircle,
Clock,
Truck,
TrendingUp,
TrendingDown,
Search,
Store,
ArrowUpDown,
} from "lucide-react";
// Типы для данных заказов
interface SupplyOrderItem {
id: string;
quantity: number;
@ -36,7 +40,6 @@ interface SupplyOrderItem {
id: string;
name: string;
article: string;
description?: string;
category?: {
id: string;
name: string;
@ -46,20 +49,15 @@ interface SupplyOrderItem {
interface SupplyOrder {
id: string;
organizationId: string;
deliveryDate: string;
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
status: string;
totalAmount: number;
totalItems: number;
fulfillmentCenterId?: string;
deliveryDate: string;
createdAt: string;
updatedAt: string;
partner: {
id: string;
name?: string;
fullName?: string;
inn: string;
address?: string;
phones?: string[];
emails?: string[];
};
@ -78,8 +76,96 @@ interface SupplyOrder {
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() {
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 { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, {
@ -94,7 +180,7 @@ export function RealSupplyOrdersTab() {
onCompleted: (data) => {
if (data.updateSupplyOrderStatus.success) {
toast.success(data.updateSupplyOrderStatus.message);
refetch(); // Обновляем список заказов
refetch();
} else {
toast.error(data.updateSupplyOrderStatus.message);
}
@ -109,14 +195,14 @@ export function RealSupplyOrdersTab() {
// Получаем ID текущей организации (поставщика)
const currentOrganizationId = user?.organization?.id;
// Фильтруем заказы где текущая организация является поставщиком (заказы К поставщику)
// Фильтруем заказы где текущая организация является поставщиком
const incomingSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter(
(order: SupplyOrder) => {
// Показываем заказы где текущий поставщик указан как поставщик (partnerId)
return order.partner.id === currentOrganizationId;
}
);
// Функции для работы с таблицей
const toggleOrderExpansion = (orderId: string) => {
const newExpanded = new Set(expandedOrders);
if (newExpanded.has(orderId)) {
@ -127,66 +213,85 @@ export function RealSupplyOrdersTab() {
setExpandedOrders(newExpanded);
};
const handleStatusUpdate = async (
orderId: string,
newStatus: SupplyOrder["status"]
) => {
const handleSort = (field: string) => {
if (sortField === field) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
} else {
setSortField(field);
setSortOrder("asc");
}
};
const handleStatusUpdate = async (orderId: string, status: string) => {
try {
await updateSupplyOrderStatus({
variables: {
id: orderId,
status: newStatus,
status,
},
});
} catch (error) {
console.error("Error updating status:", error);
console.error("Error updating order status:", error);
}
};
const getStatusBadge = (status: SupplyOrder["status"]) => {
const statusMap = {
PENDING: {
label: "Ожидает одобрения",
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
icon: Clock,
},
CONFIRMED: {
label: "Одобрена",
color: "bg-green-500/20 text-green-300 border-green-500/30",
icon: CheckCircle,
},
IN_TRANSIT: {
label: "В пути",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
icon: Truck,
},
DELIVERED: {
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];
// Фильтрация и сортировка заказов
const filteredAndSortedOrders = incomingSupplyOrders
.filter((order) => {
const searchLower = searchTerm.toLowerCase();
return (
<Badge className={`${color} border flex items-center gap-1`}>
<Icon className="h-3 w-3" />
{label}
</Badge>
order.id.toLowerCase().includes(searchLower) ||
(order.organization.name || order.organization.fullName || "")
.toLowerCase()
.includes(searchLower) ||
order.items.some(
(item) =>
item.product.name.toLowerCase().includes(searchLower) ||
item.product.article.toLowerCase().includes(searchLower)
)
);
};
})
.sort((a, b) => {
let aValue, bValue;
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();
}
if (aValue < bValue) return sortOrder === "asc" ? -1 : 1;
if (aValue > bValue) return sortOrder === "asc" ? 1 : -1;
return 0;
});
// Функции форматирования
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).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 totalAmount = incomingSupplyOrders.reduce(
(sum, order) => sum + order.totalAmount,
@ -228,6 +446,22 @@ export function RealSupplyOrdersTab() {
(order) => order.status === "IN_TRANSIT"
).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) {
return (
<div className="flex items-center justify-center h-64">
@ -250,9 +484,10 @@ export function RealSupplyOrdersTab() {
}
return (
<div className="space-y-6">
{/* Статистика входящих заявок */}
<StatsGrid>
<div className="h-full flex flex-col overflow-hidden">
{/* Статистические карточки - 30% экрана */}
<div className="flex-shrink-0 mb-4" style={{ maxHeight: "30vh" }}>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
<StatsCard
title="Всего заявок"
value={totalOrders}
@ -261,7 +496,6 @@ export function RealSupplyOrdersTab() {
iconBg="bg-orange-500/20"
subtitle="Заявки от селлеров"
/>
<StatsCard
title="Ожидают одобрения"
value={pendingOrders}
@ -270,7 +504,6 @@ export function RealSupplyOrdersTab() {
iconBg="bg-blue-500/20"
subtitle="Требуют решения"
/>
<StatsCard
title="Одобрено"
value={approvedOrders}
@ -279,7 +512,6 @@ export function RealSupplyOrdersTab() {
iconBg="bg-green-500/20"
subtitle="Готовы к отправке"
/>
<StatsCard
title="В пути"
value={inTransitOrders}
@ -288,7 +520,6 @@ export function RealSupplyOrdersTab() {
iconBg="bg-yellow-500/20"
subtitle="Доставляются"
/>
<StatsCard
title="Общая сумма"
value={formatCurrency(totalAmount)}
@ -297,7 +528,6 @@ export function RealSupplyOrdersTab() {
iconBg="bg-green-500/20"
subtitle="Стоимость заявок"
/>
<StatsCard
title="Всего единиц"
value={totalItems}
@ -306,61 +536,164 @@ export function RealSupplyOrdersTab() {
iconBg="bg-purple-500/20"
subtitle="Количество товаров"
/>
</StatsGrid>
</div>
</div>
{/* Список входящих заявок */}
{incomingSupplyOrders.length === 0 ? (
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
{/* Основная таблица - 70% экрана */}
<div className="flex-1 flex flex-col overflow-hidden">
<Card className="bg-white/10 backdrop-blur border-white/20 flex-1 flex flex-col overflow-hidden">
{/* Шапка таблицы с поиском */}
<div className="p-4 border-b border-white/10 flex-shrink-0">
<div className="flex items-center justify-between">
<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>
{/* Поиск */}
<div className="relative mx-2.5 flex-1 max-w-xs">
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-white/40" />
<Input
placeholder="Поиск по заявкам..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40"
/>
</div>
<Badge
variant="secondary"
className="bg-blue-500/20 text-blue-300 text-xs"
>
{filteredAndSortedOrders.length} заявок
</Badge>
</div>
</div>
{/* Заголовки таблицы */}
<div className="flex-shrink-0 bg-blue-500/20 border-b border-blue-500/40">
<div className="grid grid-cols-7 gap-0">
<TableHeader
field="id"
sortable
sortField={sortField}
sortOrder={sortOrder}
onSort={handleSort}
>
/ ID
</TableHeader>
<TableHeader
field="organization"
sortable
sortField={sortField}
sortOrder={sortOrder}
onSort={handleSort}
>
Заказчик
</TableHeader>
<TableHeader
field="deliveryDate"
sortable
sortField={sortField}
sortOrder={sortOrder}
onSort={handleSort}
>
Дата поставки
</TableHeader>
<TableHeader
field="totalItems"
sortable
sortField={sortField}
sortOrder={sortOrder}
onSort={handleSort}
>
Количество
</TableHeader>
<TableHeader
field="totalAmount"
sortable
sortField={sortField}
sortOrder={sortOrder}
onSort={handleSort}
>
Сумма
</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-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">
Здесь будут отображаться заявки от селлеров на поставку товаров
<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>
</Card>
</div>
) : (
<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) => {
filteredAndSortedOrders.map((order, index) => {
const colorScheme = getColorScheme(order.id);
const isOrderExpanded = expandedOrders.has(order.id);
const organizationName =
order.organization.name ||
order.organization.fullName ||
"Заказчик";
return (
<React.Fragment key={order.id}>
<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`}
>
{/* Основная строка заказа */}
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors">
<td className="p-4">
<div className="flex items-center space-x-2">
<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"
@ -371,55 +704,57 @@ export function RealSupplyOrdersTab() {
<ChevronRight className="h-4 w-4" />
)}
</button>
<span className="text-white font-medium">
<span className="text-white font-medium text-xs">
{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 ||
"Заказчик"}
<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>
</div>
<p className="text-white/60 text-sm">
Тип: {order.organization.type}
<p className="text-white/60 text-xs">
{order.organization.type}
</p>
</div>
</td>
<td className="p-4">
<div className="flex items-center space-x-2">
</div>
<div className="px-3 py-2.5 flex items-center space-x-2">
<Calendar className="h-4 w-4 text-white/40" />
<span className="text-white font-semibold">
<span className="text-white font-semibold text-sm">
{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">
<div className="px-3 py-2.5">
<span className="text-white font-semibold text-sm">
{order.totalItems} шт
</span>
</td>
<td className="p-4">
<div className="flex items-center space-x-2">
</div>
<div className="px-3 py-2.5 flex items-center space-x-2">
<DollarSign className="h-4 w-4 text-white/40" />
<span className="text-green-400 font-bold">
<span className="text-green-400 font-bold text-sm">
{formatCurrency(order.totalAmount)}
</span>
</div>
</td>
<td className="p-4">{getStatusBadge(order.status)}</td>
<td className="p-4">
<div className="flex items-center space-x-2">
<div className="px-3 py-2.5">
{getStatusBadge(order.status)}
</div>
<div className="px-3 py-2.5">
<div className="flex items-center space-x-1">
{order.status === "PENDING" && (
<>
<Button
@ -428,9 +763,9 @@ export function RealSupplyOrdersTab() {
handleStatusUpdate(order.id, "CONFIRMED")
}
disabled={updating}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-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"
>
<CheckCircle className="h-4 w-4 mr-1" />
<CheckCircle className="h-3 w-3 mr-1" />
Одобрить
</Button>
<Button
@ -439,10 +774,10 @@ export function RealSupplyOrdersTab() {
handleStatusUpdate(order.id, "CANCELLED")
}
disabled={updating}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30"
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"
>
<XCircle className="h-4 w-4 mr-1" />
Отказать
<XCircle className="h-3 w-3 mr-1" />
Отклонить
</Button>
</>
)}
@ -453,40 +788,48 @@ export function RealSupplyOrdersTab() {
handleStatusUpdate(order.id, "IN_TRANSIT")
}
disabled={updating}
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border border-yellow-500/30"
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-4 w-4 mr-1" />
<Truck className="h-3 w-3 mr-1" />
Отправить
</Button>
)}
{order.status === "CANCELLED" && (
<span className="text-red-400 text-sm">
<span className="text-red-400 text-xs">
Отклонена
</span>
)}
{order.status === "IN_TRANSIT" && (
<span className="text-yellow-400 text-sm">
<span className="text-yellow-400 text-xs">
В пути
</span>
)}
{order.status === "DELIVERED" && (
<span className="text-green-400 text-sm">
<span className="text-green-400 text-xs">
Доставлена
</span>
)}
</div>
</td>
</tr>
</div>
</div>
{/* Развернутая информация о заказе */}
{isOrderExpanded && (
<tr>
<td colSpan={8} className="p-0">
<div className="bg-white/5 border-t border-white/10">
<div
className={`${colorScheme.bg} border-t ${colorScheme.border}`}
>
<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="flex items-center space-x-2 text-white/60 text-sm">
<Calendar className="h-4 w-4" />
<span>
Дата создания: {formatDateTime(order.createdAt)}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{order.items.map((item) => (
<Card
@ -528,17 +871,14 @@ export function RealSupplyOrdersTab() {
</div>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
</div>
);
})}
</tbody>
</table>
})
)}
</div>
</Card>
)}
</div>
</div>
);
}

View File

@ -8,10 +8,19 @@ import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { useSearchParams } from "next/navigation";
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 { FulfillmentSuppliesTab } from "./fulfillment-supplies/fulfillment-supplies-tab";
import { MarketplaceSuppliesTab } from "./marketplace-supplies/marketplace-supplies-tab";
import { FulfillmentGoodsTab } from "./fulfillment-supplies/fulfillment-goods-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 {
DropdownMenu,
DropdownMenuContent,
@ -22,13 +31,14 @@ import {
export function SuppliesDashboard() {
const { getSidebarMargin } = useSidebar();
const searchParams = useSearchParams();
const [activeTab, setActiveTab] = useState("fulfillment");
const [activeTab, setActiveTab] = useState("all");
const { user } = useAuth();
// Загружаем счетчик поставок, требующих одобрения
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
pollInterval: 30000, // Обновляем каждые 30 секунд
fetchPolicy: 'cache-first',
errorPolicy: 'ignore',
fetchPolicy: "cache-first",
errorPolicy: "ignore",
});
const pendingCount = pendingData?.pendingSuppliesCount;
@ -38,10 +48,15 @@ export function SuppliesDashboard() {
useEffect(() => {
const tab = searchParams.get("tab");
if (tab === "consumables") {
setActiveTab("fulfillment"); // Устанавливаем основную вкладку "Поставки на ФФ"
setActiveTab("supplies");
} else if (tab === "goods") {
setActiveTab("goods");
}
}, [searchParams]);
// Определяем тип организации для выбора правильного компонента
const isWholesale = user?.organization?.type === "WHOLESALE";
return (
<div className="h-screen flex overflow-hidden">
<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">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
У вас есть {pendingCount.total} элемент{pendingCount.total > 1 ? (pendingCount.total < 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 ? 'и' : '') : 'а'} на партнерство`}
У вас есть {pendingCount.total} элемент
{pendingCount.total > 1
? pendingCount.total < 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>
</Alert>
)}
{/* Главные вкладки с кнопкой создания */}
{/* Основные вкладки с кнопкой создания */}
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="w-full h-full flex flex-col"
>
<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' : ''}`}>
<TabsTrigger
value="fulfillment"
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' : ''}`}
<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" : ""
}`}
>
<span className="hidden sm:inline">Поставки на ФФ</span>
<span className="sm:hidden">ФФ</span>
<TabsTrigger
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"
>
Все
</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 && (
<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}
</div>
)}
</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>
<DropdownMenu>
@ -129,25 +176,25 @@ export function SuppliesDashboard() {
</DropdownMenu>
</div>
<TabsContent
value="fulfillment"
className="mt-0 flex-1 overflow-hidden"
>
<FulfillmentSuppliesTab
defaultSubTab={
searchParams.get("tab") === "consumables"
? "supplies"
: undefined
}
<TabsContent value="all" className="mt-0 flex-1 overflow-hidden">
<AllSuppliesTab
pendingSupplyOrders={pendingCount?.supplyOrders || 0}
/>
</TabsContent>
<TabsContent value="goods" className="mt-0 flex-1 overflow-hidden">
<FulfillmentGoodsTab />
</TabsContent>
<TabsContent
value="marketplace"
value="supplies"
className="mt-0 flex-1 overflow-hidden"
>
<MarketplaceSuppliesTab />
{isWholesale ? (
<RealSupplyOrdersTab />
) : (
<SellerSupplyOrdersTab />
)}
</TabsContent>
</Tabs>
</div>