Files
sfera/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx

1512 lines
68 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import { useQuery } from "@apollo/client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
GET_MY_EMPLOYEES,
GET_MY_COUNTERPARTIES,
GET_PENDING_SUPPLIES_COUNT,
} from "@/graphql/queries";
import {
Package,
Plus,
Search,
TrendingUp,
Calendar,
Eye,
User,
Truck,
Hash,
Package2,
Boxes,
Store,
Building2,
Clock,
CheckCircle,
FileText,
Bell,
AlertTriangle,
} from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// Компонент уведомлений о непринятых поставках
function PendingSuppliesAlert() {
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
pollInterval: 30000, // Обновляем каждые 30 секунд
fetchPolicy: "cache-first",
errorPolicy: "ignore",
});
const pendingCount = pendingData?.pendingSuppliesCount?.total || 0;
const supplyOrdersCount =
pendingData?.pendingSuppliesCount?.supplyOrders || 0;
const incomingRequestsCount =
pendingData?.pendingSuppliesCount?.incomingRequests || 0;
if (pendingCount === 0) return null;
return (
<Card className="bg-gradient-to-r from-orange-500/20 to-red-500/20 backdrop-blur border-orange-400/30 p-3 mb-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-orange-500/20 rounded-full">
<Bell className="h-5 w-5 text-orange-300 animate-pulse" />
</div>
<div className="flex-1">
<h3 className="text-orange-200 font-semibold text-sm flex items-center gap-2">
<AlertTriangle className="h-4 w-4" />
Требует вашего внимания
</h3>
<div className="text-orange-100 text-xs mt-1 space-y-1">
{supplyOrdersCount > 0 && (
<p>
{supplyOrdersCount} поставок требуют вашего действия
(подтверждение/получение)
</p>
)}
{incomingRequestsCount > 0 && (
<p>
{incomingRequestsCount} заявок на партнерство ожидают ответа
</p>
)}
</div>
</div>
<div className="text-right">
<div className="bg-orange-500 text-white text-xs font-bold rounded-full w-6 h-6 flex items-center justify-center">
{pendingCount > 99 ? "99+" : pendingCount}
</div>
</div>
</div>
</Card>
);
}
// Интерфейсы для данных
interface Employee {
id: string;
firstName: string;
lastName: string;
position: string;
status: string;
}
interface Organization {
id: string;
name?: string;
fullName?: string;
type: string;
}
// Новый интерфейс для поставщика/поставщика
interface Supplier {
id: string;
name: string;
fullName?: string;
inn: string;
phone: string;
email: string;
address: string;
managerName: string;
type: "WHOLESALE" | "SUPPLIER";
products?: {
id: string;
name: string;
quantity: number;
price: number;
totalValue: number;
}[];
totalValue: number;
status: "active" | "inactive" | "pending";
}
interface Route {
id: string;
routeName: string;
fromAddress: string;
toAddress: string;
distance: number;
estimatedTime: string;
transportType: string;
cost: number;
status: "planned" | "in-transit" | "delivered" | "delayed";
suppliers: Supplier[]; // Добавляем массив поставщиков
}
interface Supply {
id: string;
supplyNumber: string;
supplyDate: string;
seller: {
id: string;
name: string;
storeName: string;
managerName: string;
phone: string;
email: string;
inn: string;
};
itemsQuantity: number;
cargoPlaces: number;
volume: number;
responsibleEmployeeId: string;
logisticsPartnerId: string;
status: string;
totalValue: number;
routes: Route[];
}
// Мок данные для поставок с новой структурой
const mockFulfillmentSupplies: Supply[] = [
{
id: "1",
supplyNumber: "ФФ-2024-001",
supplyDate: "2024-01-15",
seller: {
id: "seller1",
name: "TechStore LLC",
storeName: "ТехноМагазин",
managerName: "Иванов Иван Иванович",
phone: "+7 (495) 123-45-67",
email: "contact@techstore.ru",
inn: "7701234567",
},
itemsQuantity: 150,
cargoPlaces: 5,
volume: 12.5,
responsibleEmployeeId: "emp1",
logisticsPartnerId: "log1",
status: "planned",
totalValue: 2500000,
routes: [
{
id: "route1-1",
routeName: "Москва → Подольск (Основной)",
fromAddress: "Москва, ул. Складская, 15",
toAddress: "Подольск, ул. Логистическая, 25",
distance: 45,
estimatedTime: "1ч 20мин",
transportType: "Фура 20т",
cost: 15000,
status: "planned",
suppliers: [
{
id: "sup1-1",
name: "ООО 'ПромСтрой'",
fullName: "ООО 'ПромСтрой' - Оптовый поставщик",
inn: "7701234567890",
phone: "+7 (495) 111-22-33",
email: "info@prosmstroi.ru",
address: "Москва, ул. Строительная, 10",
managerName: "Иванов Иван",
type: "WHOLESALE",
totalValue: 1000000,
status: "active",
},
{
id: "sup1-2",
name: "ИП 'СтройМаг'",
fullName: "ИП 'СтройМаг' - Оптовый поставщик",
inn: "7709876543210",
phone: "+7 (495) 999-88-77",
email: "orders@stroymag.ru",
address: "Москва, ул. Магистральная, 5",
managerName: "Петров Петр",
type: "SUPPLIER",
totalValue: 500000,
status: "inactive",
},
],
},
{
id: "route1-2",
routeName: "Подольск → Тула (Резервный)",
fromAddress: "Подольск, ул. Логистическая, 25",
toAddress: "Тула, ул. Промышленная, 8",
distance: 180,
estimatedTime: "3ч 15мин",
transportType: "Газель",
cost: 8500,
status: "planned",
suppliers: [
{
id: "sup1-3",
name: "ООО 'СтройТорг'",
fullName: "ООО 'СтройТорг' - Оптовый поставщик",
inn: "7701123456789",
phone: "+7 (495) 222-33-44",
email: "sales@stroitorg.ru",
address: "Подольск, ул. Логистическая, 25",
managerName: "Сидоров Сидор",
type: "WHOLESALE",
totalValue: 1500000,
status: "active",
},
],
},
],
},
{
id: "2",
supplyNumber: "ФФ-2024-002",
supplyDate: "2024-01-12",
seller: {
id: "seller2",
name: "Apple Reseller",
storeName: "ЭплСтор",
managerName: "Петров Петр Петрович",
phone: "+7 (495) 987-65-43",
email: "orders@applereseller.ru",
inn: "7709876543",
},
itemsQuantity: 75,
cargoPlaces: 3,
volume: 8.2,
responsibleEmployeeId: "emp2",
logisticsPartnerId: "log2",
status: "in-transit",
totalValue: 3750000,
routes: [
{
id: "route2-1",
routeName: "СПб → Москва (Экспресс)",
fromAddress: "Санкт-Петербург, пр. Обуховской Обороны, 120",
toAddress: "Москва, МКАД 47км",
distance: 635,
estimatedTime: "8ч 45мин",
transportType: "Фура 40т",
cost: 45000,
status: "in-transit",
suppliers: [
{
id: "sup2-1",
name: "ООО 'ЭлектроТорг'",
fullName: "ООО 'ЭлектроТорг' - Оптовый поставщик",
inn: "7701234567890",
phone: "+7 (495) 333-44-55",
email: "info@elektortorg.ru",
address: "Санкт-Петербург, ул. Электронная, 10",
managerName: "Иванов Иван",
type: "WHOLESALE",
totalValue: 2000000,
status: "active",
},
],
},
],
},
{
id: "3",
supplyNumber: "ФФ-2024-003",
supplyDate: "2024-01-10",
seller: {
id: "seller3",
name: "Audio World",
storeName: "АудиоМир",
managerName: "Сидоров Сидор Сидорович",
phone: "+7 (495) 555-12-34",
email: "info@audioworld.ru",
inn: "7705551234",
},
itemsQuantity: 200,
cargoPlaces: 8,
volume: 15.7,
responsibleEmployeeId: "emp1",
logisticsPartnerId: "log1",
status: "delivered",
totalValue: 2800000,
routes: [
{
id: "route3-1",
routeName: "Казань → Москва (Основной)",
fromAddress: "Казань, ул. Портовая, 18",
toAddress: "Москва, ул. Складская, 15",
distance: 815,
estimatedTime: "12ч 30мин",
transportType: "Фура 20т",
cost: 38000,
status: "delivered",
suppliers: [
{
id: "sup3-1",
name: "ООО 'МеталлСтрой'",
fullName: "ООО 'МеталлСтрой' - Оптовый поставщик",
inn: "7701234567890",
phone: "+7 (495) 444-55-66",
email: "sales@metallstroi.ru",
address: "Казань, ул. Портовая, 18",
managerName: "Иванов Иван",
type: "WHOLESALE",
totalValue: 1800000,
status: "active",
},
],
},
{
id: "route3-2",
routeName: "Москва → Тверь (Доставка)",
fromAddress: "Москва, ул. Складская, 15",
toAddress: "Тверь, ул. Вагжанова, 7",
distance: 170,
estimatedTime: "2ч 45мин",
transportType: "Газель",
cost: 12000,
status: "delivered",
suppliers: [
{
id: "sup3-2",
name: "ИП 'СтройМаг'",
fullName: "ИП 'СтройМаг' - Оптовый поставщик",
inn: "7709876543210",
phone: "+7 (495) 999-88-77",
email: "orders@stroymag.ru",
address: "Москва, ул. Складская, 15",
managerName: "Петров Петр",
type: "SUPPLIER",
totalValue: 1000000,
status: "inactive",
},
],
},
],
},
// Добавляем больше тестовых данных для демонстрации вкладок
{
id: "4",
supplyNumber: "ФФ-2024-004",
supplyDate: "2024-01-20",
seller: {
id: "seller4",
name: "Gaming Store",
storeName: "ГеймингМир",
managerName: "Игоров Игорь Игоревич",
phone: "+7 (495) 777-88-99",
email: "info@gamingworld.ru",
inn: "7707778899",
},
itemsQuantity: 120,
cargoPlaces: 4,
volume: 10.3,
responsibleEmployeeId: "emp1",
logisticsPartnerId: "log1",
status: "planned", // Новые
totalValue: 1800000,
routes: [],
},
{
id: "5",
supplyNumber: "ФФ-2024-005",
supplyDate: "2024-01-18",
seller: {
id: "seller5",
name: "Fashion Store",
storeName: "МодныйСтиль",
managerName: "Стильнов Стиль Стильнович",
phone: "+7 (495) 666-77-88",
email: "info@fashionstore.ru",
inn: "7706667788",
},
itemsQuantity: 85,
cargoPlaces: 2,
volume: 6.5,
responsibleEmployeeId: "emp2",
logisticsPartnerId: "log2",
status: "in-processing", // Принято
totalValue: 1200000,
routes: [],
},
{
id: "6",
supplyNumber: "ФФ-2024-006",
supplyDate: "2024-01-22",
seller: {
id: "seller6",
name: "Sports Store",
storeName: "СпортМастер",
managerName: "Спортов Спорт Спортович",
phone: "+7 (495) 555-66-77",
email: "info@sportsstore.ru",
inn: "7705556677",
},
itemsQuantity: 95,
cargoPlaces: 3,
volume: 8.7,
responsibleEmployeeId: "emp1",
logisticsPartnerId: "log1",
status: "planned", // Новые
totalValue: 1500000,
routes: [],
},
];
export function FulfillmentGoodsTab() {
const [activeTab, setActiveTab] = useState("new");
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [expandedSellers, setExpandedSellers] = useState<Set<string>>(
new Set()
);
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(
new Set()
);
const [expandedRoutes, setExpandedRoutes] = useState<Set<string>>(new Set());
const [expandedSuppliers, setExpandedSuppliers] = useState<Set<string>>(
new Set()
);
// Загружаем сотрудников для селектора ответственных
const { data: employeesData, loading: employeesLoading } =
useQuery(GET_MY_EMPLOYEES);
// Загружаем партнеров-логистов
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(
GET_MY_COUNTERPARTIES
);
const employees: Employee[] = employeesData?.myEmployees || [];
const logisticsPartners = (counterpartiesData?.myCounterparties || []).filter(
(org: Organization) => org.type === "LOGIST"
);
const toggleSellerExpansion = (sellerId: string) => {
if (!sellerId) {
console.error("SellerId is undefined or null");
return;
}
const newExpanded = new Set(expandedSellers);
if (newExpanded.has(sellerId)) {
newExpanded.delete(sellerId);
} else {
newExpanded.add(sellerId);
}
setExpandedSellers(newExpanded);
};
const toggleSupplyExpansion = (supplyId: string) => {
if (!supplyId) {
console.error("SupplyId is undefined or null");
return;
}
const newExpanded = new Set(expandedSupplies);
if (newExpanded.has(supplyId)) {
newExpanded.delete(supplyId);
} else {
newExpanded.add(supplyId);
}
setExpandedSupplies(newExpanded);
};
const toggleRouteExpansion = (routeId: string) => {
if (!routeId) {
console.error("RouteId is undefined or null");
return;
}
const newExpanded = new Set(expandedRoutes);
if (newExpanded.has(routeId)) {
newExpanded.delete(routeId);
} else {
newExpanded.add(routeId);
}
setExpandedRoutes(newExpanded);
};
const toggleSupplierExpansion = (supplierId: string) => {
if (!supplierId) {
console.error("SupplierId is undefined or null");
return;
}
const newExpanded = new Set(expandedSuppliers);
if (newExpanded.has(supplierId)) {
newExpanded.delete(supplierId);
} else {
newExpanded.add(supplierId);
}
setExpandedSuppliers(newExpanded);
};
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 getStatusBadge = (status: string) => {
const statusConfig = {
planned: {
color: "text-blue-300 border-blue-400/30",
label: "Запланировано",
},
"in-transit": {
color: "text-yellow-300 border-yellow-400/30",
label: "В пути",
},
delivered: {
color: "text-green-300 border-green-400/30",
label: "Доставлено",
},
"in-processing": {
color: "text-purple-300 border-purple-400/30",
label: "Обрабатывается",
},
};
const config =
statusConfig[status as keyof typeof statusConfig] || statusConfig.planned;
return (
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
{config.label}
</Badge>
);
};
const getRouteStatusBadge = (status: string) => {
const statusConfig = {
planned: {
color: "text-blue-300 border-blue-400/30 bg-blue-500/10",
label: "Запланирован",
},
"in-transit": {
color: "text-yellow-300 border-yellow-400/30 bg-yellow-500/10",
label: "В пути",
},
delivered: {
color: "text-green-300 border-green-400/30 bg-green-500/10",
label: "Доставлен",
},
delayed: {
color: "text-red-300 border-red-400/30 bg-red-500/10",
label: "Задержка",
},
};
const config =
statusConfig[status as keyof typeof statusConfig] || statusConfig.planned;
return (
<Badge variant="outline" className={`${config.color} text-xs`}>
{config.label}
</Badge>
);
};
const getSupplierStatusBadge = (status: string) => {
const statusConfig = {
active: {
color: "text-green-300 border-green-400/30 bg-green-500/10",
label: "Активен",
},
inactive: {
color: "text-gray-300 border-gray-400/30 bg-gray-500/10",
label: "Неактивен",
},
pending: {
color: "text-yellow-300 border-yellow-400/30 bg-yellow-500/10",
label: "Ожидает",
},
};
const config =
statusConfig[status as keyof typeof statusConfig] || statusConfig.active;
return (
<Badge variant="outline" className={`${config.color} text-xs`}>
{config.label}
</Badge>
);
};
const getEmployeeName = (employeeId: string) => {
const employee = employees.find((emp) => emp.id === employeeId);
return employee
? `${employee.firstName} ${employee.lastName}`
: "Не назначен";
};
const getLogisticsPartnerName = (partnerId: string) => {
const partner = logisticsPartners.find(
(p: Organization) => p.id === partnerId
);
return partner
? partner.name || partner.fullName || "Без названия"
: "Не выбран";
};
const getFilteredSuppliesByTab = (tabName: string) => {
let supplies = mockFulfillmentSupplies;
// Фильтрация по вкладке
switch (tabName) {
case "new":
supplies = supplies.filter((supply) => supply.status === "planned");
break;
case "receiving":
supplies = supplies.filter((supply) => supply.status === "in-transit");
break;
case "received":
supplies = supplies.filter(
(supply) =>
supply.status === "delivered" || supply.status === "in-processing"
);
break;
default:
break;
}
// Дополнительная фильтрация по поиску и статусу
return supplies.filter((supply) => {
const matchesSearch =
supply.supplyNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
supply.seller.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
supply.seller.storeName
.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
supply.seller.inn.includes(searchTerm);
const matchesStatus =
statusFilter === "all" || supply.status === statusFilter;
return matchesSearch && matchesStatus;
});
};
const filteredSupplies = getFilteredSuppliesByTab(activeTab);
const getTotalValue = () => {
return filteredSupplies.reduce((sum, supply) => sum + supply.totalValue, 0);
};
const getTotalQuantity = () => {
return filteredSupplies.reduce(
(sum, supply) => sum + supply.itemsQuantity,
0
);
};
const getTotalVolume = () => {
return filteredSupplies.reduce((sum, supply) => sum + supply.volume, 0);
};
return (
<div className="h-full flex flex-col p-2 xl:p-4">
{/* Уведомления о непринятых поставках */}
<PendingSuppliesAlert />
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="h-full flex flex-col"
>
{/* Вкладки товаров */}
<TabsList className="grid w-full grid-cols-3 bg-white/10 backdrop-blur border-white/10 flex-shrink-0 h-8 xl:h-10 mb-2 xl:mb-4">
<TabsTrigger
value="new"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 xl:gap-2 text-xs xl:text-sm"
>
<Clock className="h-3 w-3 xl:h-4 xl:w-4" />
<span className="hidden sm:inline">Новые</span>
<span className="sm:hidden">Н</span>
</TabsTrigger>
<TabsTrigger
value="receiving"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 xl:gap-2 text-xs xl:text-sm"
>
<FileText className="h-3 w-3 xl:h-4 xl:w-4" />
<span className="hidden sm:inline">Приёмка</span>
<span className="sm:hidden">П</span>
</TabsTrigger>
<TabsTrigger
value="received"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 xl:gap-2 text-xs xl:text-sm"
>
<CheckCircle className="h-3 w-3 xl:h-4 xl:w-4" />
<span className="hidden sm:inline">Принято</span>
<span className="sm:hidden">Пр</span>
</TabsTrigger>
</TabsList>
<TabsContent value="new" className="flex-1 overflow-hidden">
<TabContent tabName="new" />
</TabsContent>
<TabsContent value="receiving" className="flex-1 overflow-hidden">
<TabContent tabName="receiving" />
</TabsContent>
<TabsContent value="received" className="flex-1 overflow-hidden">
<TabContent tabName="received" />
</TabsContent>
</Tabs>
</div>
);
function TabContent({ tabName }: { tabName: string }) {
const tabFilteredSupplies = getFilteredSuppliesByTab(tabName);
const getTabTotalValue = () => {
return tabFilteredSupplies.reduce(
(sum, supply) => sum + supply.totalValue,
0
);
};
const getTabTotalQuantity = () => {
return tabFilteredSupplies.reduce(
(sum, supply) => sum + supply.itemsQuantity,
0
);
};
const getTabTotalVolume = () => {
return tabFilteredSupplies.reduce(
(sum, supply) => sum + supply.volume,
0
);
};
return (
<div className="h-full flex flex-col space-y-2 xl:space-y-4">
{/* Статистика с кнопкой */}
<div className="flex flex-col xl:flex-row xl:items-center xl:justify-between gap-3 xl:gap-4">
<div className="grid grid-cols-2 xl:grid-cols-4 gap-2 xl:gap-3 flex-1">
<Card className="glass-card p-2 xl:p-3 h-[50px] xl:h-[60px]">
<div className="flex items-center space-x-1.5 xl:space-x-2 h-full">
<div className="p-1 xl:p-1.5 bg-blue-500/20 rounded">
<Package className="h-2.5 w-2.5 xl:h-3 xl:w-3 text-blue-400" />
</div>
<div>
<p className="text-white/60 text-[10px] xl:text-xs">
Поставок
</p>
<p className="text-sm xl:text-lg font-bold text-white">
{tabFilteredSupplies.length}
</p>
</div>
</div>
</Card>
<Card className="glass-card p-2 xl:p-3 h-[50px] xl:h-[60px]">
<div className="flex items-center space-x-1.5 xl:space-x-2 h-full">
<div className="p-1 xl:p-1.5 bg-green-500/20 rounded">
<TrendingUp className="h-2.5 w-2.5 xl:h-3 xl:w-3 text-green-400" />
</div>
<div className="min-w-0 flex-1">
<p className="text-white/60 text-[10px] xl:text-xs">
Стоимость
</p>
<p className="text-sm xl:text-lg font-bold text-white truncate">
{formatCurrency(getTabTotalValue())}
</p>
</div>
</div>
</Card>
<Card className="glass-card p-2 xl:p-3 h-[50px] xl:h-[60px]">
<div className="flex items-center space-x-1.5 xl:space-x-2 h-full">
<div className="p-1 xl:p-1.5 bg-purple-500/20 rounded">
<Package2 className="h-2.5 w-2.5 xl:h-3 xl:w-3 text-purple-400" />
</div>
<div>
<p className="text-white/60 text-[10px] xl:text-xs">
Товаров
</p>
<p className="text-sm xl:text-lg font-bold text-white">
{getTabTotalQuantity()} ед.
</p>
</div>
</div>
</Card>
<Card className="glass-card p-2 xl:p-3 h-[50px] xl:h-[60px]">
<div className="flex items-center space-x-1.5 xl:space-x-2 h-full">
<div className="p-1 xl:p-1.5 bg-orange-500/20 rounded">
<Boxes className="h-2.5 w-2.5 xl:h-3 xl:w-3 text-orange-400" />
</div>
<div>
<p className="text-white/60 text-[10px] xl:text-xs">Объём</p>
<p className="text-sm xl:text-lg font-bold text-white">
{getTabTotalVolume().toFixed(1)} м³
</p>
</div>
</div>
</Card>
</div>
<Button
size="sm"
className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white text-xs xl:text-sm px-3 xl:px-6 h-[50px] xl:h-[60px] whitespace-nowrap w-full xl:w-auto"
>
<Plus className="h-3 w-3 xl:h-4 xl:w-4 mr-1 xl:mr-2" />
<span className="hidden sm:inline">Создать поставку</span>
<span className="sm:hidden">Создать</span>
</Button>
</div>
{/* Фильтры */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 xl:gap-3">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-2 xl:left-3 top-1/2 transform -translate-y-1/2 h-3 w-3 xl:h-4 xl:w-4 text-white/40" />
<Input
placeholder="Поиск по номеру, магазину, ИНН..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="glass-input pl-8 xl:pl-10 text-white placeholder:text-white/40 text-xs xl:text-sm h-8 xl:h-auto"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="glass-input text-white text-xs xl:text-sm px-2 xl:px-3 py-1.5 xl:py-2 rounded-lg bg-white/5 border border-white/10 h-8 xl:h-auto"
>
<option value="all">Все статусы</option>
<option value="planned">Запланировано</option>
<option value="in-transit">В пути</option>
<option value="delivered">Доставлено</option>
<option value="in-processing">Обрабатывается</option>
</select>
</div>
{/* Список поставок */}
<div className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto space-y-2 xl:space-y-3">
{tabFilteredSupplies.map((supply, index) => {
const isSellerExpanded = expandedSellers.has(supply.seller.id);
const isSupplyExpanded = expandedSupplies.has(supply.id);
return (
<Card
key={supply.id}
className="glass-card p-2 xl:p-4 hover:bg-white/10 transition-colors"
>
<div className="space-y-2 xl:space-y-3">
{/* Компактный блок с названием магазина */}
<div
className="flex flex-col xl:flex-row xl:items-center xl:justify-between bg-white/5 rounded-lg p-1.5 xl:p-2 cursor-pointer hover:bg-white/10 transition-colors gap-2 xl:gap-0"
onClick={() => {
if (supply?.seller?.id) {
toggleSellerExpansion(supply.seller.id);
} else {
console.error(
"Supply seller or seller.id is undefined",
supply
);
}
}}
>
<div className="flex items-center gap-2 xl:gap-3">
<div className="p-1 xl:p-1.5 bg-green-500/20 rounded">
<Store className="h-2.5 w-2.5 xl:h-3 xl:w-3 text-green-400" />
</div>
<div className="flex flex-col xl:flex-row xl:items-center gap-1 xl:gap-2 min-w-0 flex-1">
<span className="text-white font-medium text-xs xl:text-sm truncate">
{supply.seller.storeName}
</span>
<div className="flex items-center gap-1 xl:gap-2 text-[10px] xl:text-xs">
<span className="text-white/80 truncate">
{supply.seller.managerName}
</span>
<span className="text-white/60 hidden xl:inline">
{" "}
{" "}
</span>
<span className="text-white/80 truncate">
{supply.seller.phone}
</span>
</div>
<div className="flex items-center gap-1 xl:hidden">
<a
href={`https://t.me/${supply.seller.phone.replace(
/[^\d]/g,
""
)}`}
target="_blank"
rel="noopener noreferrer"
className="p-1 bg-blue-500/20 rounded hover:bg-blue-500/30 transition-colors"
title="Написать в Telegram"
onClick={(e) => e.stopPropagation()}
>
<svg
className="h-3 w-3 text-blue-400"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
</svg>
</a>
<a
href={`https://wa.me/${supply.seller.phone.replace(
/[^\d]/g,
""
)}`}
target="_blank"
rel="noopener noreferrer"
className="p-1 bg-green-500/20 rounded hover:bg-green-500/30 transition-colors"
title="Написать в WhatsApp"
onClick={(e) => e.stopPropagation()}
>
<svg
className="h-3 w-3 text-green-400"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0020.885 3.488" />
</svg>
</a>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span className="text-white/60 text-xs">
Номер поставки
</span>
<span className="text-primary font-semibold text-sm">
{supply.supplyNumber}
</span>
</div>
</div>
</div>
{/* Скрытая детальная информация о магазине */}
{isSellerExpanded && (
<div className="bg-white/5 rounded-lg p-3 space-y-2">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-white/60 text-xs">
Юридическое название
</p>
<p className="text-white text-sm">
{supply.seller.name}
</p>
</div>
<div>
<p className="text-white/60 text-xs">ИНН</p>
<p className="text-white text-sm font-mono">
{supply.seller.inn}
</p>
</div>
</div>
<div>
<p className="text-white/60 text-xs">Email</p>
<p className="text-white text-sm">
{supply.seller.email}
</p>
</div>
</div>
)}
{/* Единый блок со всеми параметрами в одной строке */}
<div
className="bg-white/5 rounded-lg p-4 cursor-pointer hover:bg-white/10 transition-colors"
onClick={() => {
if (supply?.id) {
toggleSupplyExpansion(supply.id);
} else {
console.error(
"Supply or supply.id is undefined",
supply
);
}
}}
>
<div className="grid grid-cols-2 lg:grid-cols-9 gap-4 items-center">
{/* Порядковый номер */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1"></p>
<div className="flex items-center justify-center">
<div className="bg-blue-500/20 rounded-full w-8 h-8 flex items-center justify-center">
<span className="text-blue-300 font-bold text-sm">
{tabFilteredSupplies.length - index}
</span>
</div>
</div>
</div>
{/* Дата поставки */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">Дата</p>
<p className="text-white font-semibold text-sm">
{formatDate(supply.supplyDate)}
</p>
</div>
{/* Количество товаров */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">Товаров</p>
<p className="text-white font-semibold text-sm">
{supply.itemsQuantity}
</p>
</div>
{/* Количество мест */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">Мест</p>
<p className="text-white font-semibold text-sm">
{supply.cargoPlaces}
</p>
</div>
{/* Объём */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">Объём</p>
<p className="text-white font-semibold text-sm">
{supply.volume} м³
</p>
</div>
{/* Стоимость */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">
Стоимость
</p>
<p className="text-green-400 font-semibold text-sm">
{formatCurrency(supply.totalValue)}
</p>
</div>
{/* Ответственный сотрудник */}
<div
className="text-center"
onClick={(e) => e.stopPropagation()}
>
<p className="text-white/60 text-xs mb-1">
Ответственный
</p>
<Select defaultValue="">
<SelectTrigger className="h-6 w-full glass-input bg-white/10 border-white/20 text-white hover:bg-white/15 focus:bg-white/15 focus:ring-1 focus:ring-purple-400/50 text-xs px-2">
<SelectValue placeholder="не выбрано" />
</SelectTrigger>
<SelectContent className="bg-gray-900/95 backdrop-blur border-white/20 text-white">
{employees.map((employee) => (
<SelectItem
key={employee.id}
value={employee.id}
className="text-white hover:bg-white/10 focus:bg-white/10 text-xs"
>
<div className="flex flex-col">
<span className="font-medium">
{employee.firstName} {employee.lastName}
</span>
<span className="text-xs text-white/60">
{employee.position}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Логистический партнер */}
<div
className="text-center"
onClick={(e) => e.stopPropagation()}
>
<p className="text-white/60 text-xs mb-1">
Логистика
</p>
<Select defaultValue="">
<SelectTrigger className="h-6 w-full glass-input bg-white/10 border-white/20 text-white hover:bg-white/15 focus:bg-white/15 focus:ring-1 focus:ring-orange-400/50 text-xs px-2">
<SelectValue placeholder="не выбрано" />
</SelectTrigger>
<SelectContent className="bg-gray-900/95 backdrop-blur border-white/20 text-white">
{logisticsPartners.map(
(partner: Organization) => (
<SelectItem
key={partner.id}
value={partner.id}
className="text-white hover:bg-white/10 focus:bg-white/10 text-xs"
>
<div className="flex flex-col">
<span className="font-medium">
{partner.name ||
partner.fullName ||
"Без названия"}
</span>
<span className="text-xs text-white/60">
Логистический партнер
</span>
</div>
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
{/* Статус */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">Статус</p>
<div className="flex justify-center">
{getStatusBadge(supply.status)}
</div>
</div>
</div>
{/* Второй уровень - Маршруты */}
{isSupplyExpanded &&
supply.routes &&
supply.routes.length > 0 && (
<div className="mt-4 pt-4 border-t border-white/10">
<div className="mb-3">
<h4 className="text-white font-medium text-sm flex items-center gap-2">
<Truck className="h-4 w-4 text-orange-400" />
Маршруты ({supply.routes.length})
</h4>
</div>
<div className="space-y-2">
{supply.routes.map((route) => {
const isRouteExpanded = expandedRoutes.has(
route.id
);
return (
<div
key={route.id}
className="bg-white/5 rounded-lg p-3 border border-white/10"
>
<div
className="grid grid-cols-1 lg:grid-cols-8 gap-3 items-center cursor-pointer hover:bg-white/5 transition-colors rounded-lg p-1"
onClick={() => {
if (route && route.id) {
toggleRouteExpansion(route.id);
} else {
console.error(
"Route or route.id is undefined",
route
);
}
}}
>
{/* Название маршрута */}
<div className="lg:col-span-2">
<p className="text-white/60 text-xs mb-1">
Маршрут
</p>
<p className="text-white font-medium text-sm">
{route?.routeName || "Без названия"}
</p>
</div>
{/* Откуда */}
<div>
<p className="text-white/60 text-xs mb-1">
Откуда
</p>
<p className="text-white text-xs">
{route?.fromAddress || "Не указано"}
</p>
</div>
{/* Куда */}
<div>
<p className="text-white/60 text-xs mb-1">
Куда
</p>
<p className="text-white text-xs">
{route?.toAddress || "Не указано"}
</p>
</div>
{/* Расстояние */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">
Расстояние
</p>
<p className="text-white font-semibold text-sm">
{route?.distance || 0} км
</p>
</div>
{/* Время в пути */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">
Время
</p>
<p className="text-white font-semibold text-sm">
{route?.estimatedTime || "Не указано"}
</p>
</div>
{/* Транспорт */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">
Транспорт
</p>
<p className="text-white text-xs">
{route.transportType}
</p>
</div>
{/* Стоимость и статус */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">
Стоимость
</p>
<p className="text-green-400 font-semibold text-sm mb-1">
{formatCurrency(route.cost)}
</p>
{getRouteStatusBadge(route.status)}
</div>
</div>
{/* Третий уровень - Поставщики/Поставщики */}
{isRouteExpanded &&
route?.suppliers &&
Array.isArray(route.suppliers) &&
route.suppliers.length > 0 && (
<div className="mt-4 pt-4 border-t border-white/10">
<div className="mb-3">
<h5 className="text-white font-medium text-sm flex items-center gap-2">
<Building2 className="h-4 w-4 text-purple-400" />
Поставщики/Поставщики (
{route?.suppliers?.length || 0})
</h5>
</div>
<div className="space-y-2">
{(route?.suppliers || []).map(
(supplier) => {
const isSupplierExpanded =
expandedSuppliers.has(
supplier.id
);
return (
<div
key={supplier.id}
className="bg-white/5 rounded-lg p-3 border border-white/5"
>
<div
className="grid grid-cols-1 lg:grid-cols-7 gap-3 items-center cursor-pointer hover:bg-white/5 transition-colors rounded-lg p-1"
onClick={() => {
if (
supplier &&
supplier.id
) {
toggleSupplierExpansion(
supplier.id
);
} else {
console.error(
"Supplier or supplier.id is undefined",
supplier
);
}
}}
>
{/* Название поставщика */}
<div className="lg:col-span-2">
<p className="text-white/60 text-xs mb-1">
Поставщик
</p>
<p className="text-white font-medium text-sm">
{supplier?.name ||
"Без названия"}
</p>
</div>
{/* ИНН */}
<div>
<p className="text-white/60 text-xs mb-1">
ИНН
</p>
<p className="text-white text-xs font-mono">
{supplier?.inn ||
"Не указан"}
</p>
</div>
{/* Тип */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">
Тип
</p>
<Badge
variant="outline"
className={`text-xs ${
supplier?.type ===
"WHOLESALE"
? "text-blue-300 border-blue-400/30 bg-blue-500/10"
: "text-orange-300 border-orange-400/30 bg-orange-500/10"
}`}
>
{supplier?.type ===
"WHOLESALE"
? "Поставщик"
: "Поставщик"}
</Badge>
</div>
{/* Менеджер */}
<div>
<p className="text-white/60 text-xs mb-1">
Менеджер
</p>
<p className="text-white text-xs">
{supplier?.managerName ||
"Не указан"}
</p>
</div>
{/* Стоимость */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">
Стоимость
</p>
<p className="text-green-400 font-semibold text-sm">
{formatCurrency(
supplier?.totalValue ||
0
)}
</p>
</div>
{/* Статус */}
<div className="text-center">
<p className="text-white/60 text-xs mb-1">
Статус
</p>
{getSupplierStatusBadge(
supplier?.status ||
"active"
)}
</div>
</div>
{/* Детальная информация о поставщике */}
{isSupplierExpanded && (
<div className="mt-3 pt-3 border-t border-white/5">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div>
<p className="text-white/60 text-xs mb-1">
Полное название
</p>
<p className="text-white text-sm">
{supplier?.fullName ||
supplier?.name ||
"Не указано"}
</p>
</div>
<div>
<p className="text-white/60 text-xs mb-1">
Телефон
</p>
<div className="flex items-center gap-2">
<p className="text-white text-sm">
{supplier?.phone ||
"Не указан"}
</p>
<div className="flex items-center gap-1">
<a
href={`https://t.me/${(
supplier?.phone ||
""
).replace(
/[^\d]/g,
""
)}`}
target="_blank"
rel="noopener noreferrer"
className="p-1 bg-blue-500/20 rounded hover:bg-blue-500/30 transition-colors"
title="Написать в Telegram"
>
<svg
className="h-3 w-3 text-blue-400"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
</svg>
</a>
<a
href={`https://wa.me/${(
supplier?.phone ||
""
).replace(
/[^\d]/g,
""
)}`}
target="_blank"
rel="noopener noreferrer"
className="p-1 bg-green-500/20 rounded hover:bg-green-500/30 transition-colors"
title="Написать в WhatsApp"
>
<svg
className="h-3 w-3 text-green-400"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0020.885 3.488" />
</svg>
</a>
</div>
</div>
</div>
<div>
<p className="text-white/60 text-xs mb-1">
Email
</p>
<p className="text-white text-sm">
{supplier?.email ||
"Не указан"}
</p>
</div>
</div>
<div className="mt-3">
<p className="text-white/60 text-xs mb-1">
Адрес
</p>
<p className="text-white text-sm">
{supplier?.address ||
"Не указан"}
</p>
</div>
</div>
)}
</div>
);
}
)}
</div>
</div>
)}
</div>
);
})}
</div>
</div>
)}
</div>
</div>
</Card>
);
})}
</div>
</div>
</div>
);
}
}