Добавлено обновление кэша для расходников фулфилмента в компонентах создания и отображения заказов. Реализованы новые GraphQL запросы для получения данных о расходниках. Удалены устаревшие компоненты уведомлений о непринятых поставках для упрощения интерфейса. Оптимизирована логика отображения и обновления данных о заказах.

This commit is contained in:
Veronika Smirnova
2025-07-29 17:45:29 +03:00
parent 7877f61d5a
commit 50438bb21f
18 changed files with 3693 additions and 191 deletions

View File

@ -0,0 +1,412 @@
"use client";
import React, { useState, useMemo, useCallback } from "react";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { useQuery } from "@apollo/client";
import { GET_MY_SUPPLIES } from "@/graphql/queries";
import {
Package,
Wrench,
AlertTriangle,
CheckCircle,
Clock,
} from "lucide-react";
import { toast } from "sonner";
// Новые компоненты
import { SuppliesHeader } from "./supplies-header";
import { SuppliesStats } from "./supplies-stats";
import { SuppliesGrid } from "./supplies-grid";
import { SuppliesList } from "./supplies-list";
// Типы
import {
Supply,
FilterState,
SortState,
ViewMode,
GroupBy,
StatusConfig,
} from "./types";
// Статусы расходников с цветами
const STATUS_CONFIG = {
available: {
label: "Доступен",
color: "bg-green-500/20 text-green-300",
icon: CheckCircle,
},
"low-stock": {
label: "Мало на складе",
color: "bg-yellow-500/20 text-yellow-300",
icon: AlertTriangle,
},
"out-of-stock": {
label: "Нет в наличии",
color: "bg-red-500/20 text-red-300",
icon: AlertTriangle,
},
"in-transit": {
label: "В пути",
color: "bg-blue-500/20 text-blue-300",
icon: Clock,
},
reserved: {
label: "Зарезервирован",
color: "bg-purple-500/20 text-purple-300",
icon: Package,
},
} as const;
export function FulfillmentSuppliesPage() {
const { getSidebarMargin } = useSidebar();
// Состояния
const [viewMode, setViewMode] = useState<ViewMode>("grid");
const [filters, setFilters] = useState<FilterState>({
search: "",
category: "",
status: "",
supplier: "",
lowStock: false,
});
const [sort, setSort] = useState<SortState>({
field: "name",
direction: "asc",
});
const [showFilters, setShowFilters] = useState(false);
const [groupBy, setGroupBy] = useState<GroupBy>("none");
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(
new Set()
);
// Загрузка данных
const {
data: suppliesData,
loading,
error,
refetch,
} = useQuery(GET_MY_SUPPLIES, {
fetchPolicy: "cache-and-network",
onError: (error) => {
toast.error("Ошибка загрузки расходников: " + error.message);
},
});
const supplies: Supply[] = suppliesData?.mySupplies || [];
// Логирование для отладки
console.log("🔥🔥🔥 FULFILLMENT SUPPLIES PAGE DATA 🔥🔥🔥", {
suppliesCount: supplies.length,
supplies: supplies.map((s) => ({
id: s.id,
name: s.name,
status: s.status,
currentStock: s.currentStock,
quantity: s.quantity,
})),
});
// Функции
const getStatusConfig = useCallback((status: string): StatusConfig => {
return (
STATUS_CONFIG[status as keyof typeof STATUS_CONFIG] ||
STATUS_CONFIG.available
);
}, []);
const getSupplyDeliveries = useCallback(
(supply: Supply): Supply[] => {
return supplies.filter(
(s) => s.name === supply.name && s.category === supply.category
);
},
[supplies]
);
// Объединение одинаковых расходников
const consolidatedSupplies = useMemo(() => {
const grouped = supplies.reduce((acc, supply) => {
const key = `${supply.name}-${supply.category}`;
if (!acc[key]) {
acc[key] = {
...supply,
currentStock: 0,
quantity: 0, // Общее количество поставленного (= заказанному)
price: 0,
totalCost: 0, // Общая стоимость
shippedQuantity: 0, // Общее отправленное количество
};
}
// Суммируем поставленное количество (заказано = поставлено)
acc[key].quantity += supply.quantity;
// Суммируем отправленное количество
acc[key].shippedQuantity += supply.shippedQuantity || 0;
// Остаток = Поставлено - Отправлено
// Если ничего не отправлено, то остаток = поставлено
acc[key].currentStock = acc[key].quantity - acc[key].shippedQuantity;
// Рассчитываем общую стоимость (количество × цена)
acc[key].totalCost += supply.quantity * supply.price;
// Средневзвешенная цена за единицу
if (acc[key].quantity > 0) {
acc[key].price = acc[key].totalCost / acc[key].quantity;
}
return acc;
}, {} as Record<string, Supply & { totalCost: number }>);
return Object.values(grouped);
}, [supplies]);
// Фильтрация и сортировка
const filteredAndSortedSupplies = useMemo(() => {
let filtered = consolidatedSupplies.filter((supply) => {
const matchesSearch =
supply.name.toLowerCase().includes(filters.search.toLowerCase()) ||
supply.description.toLowerCase().includes(filters.search.toLowerCase());
const matchesCategory =
!filters.category || supply.category === filters.category;
const matchesStatus = !filters.status || supply.status === filters.status;
const matchesSupplier =
!filters.supplier ||
supply.supplier.toLowerCase().includes(filters.supplier.toLowerCase());
const matchesLowStock =
!filters.lowStock ||
(supply.currentStock <= supply.minStock && supply.currentStock > 0);
return (
matchesSearch &&
matchesCategory &&
matchesStatus &&
matchesSupplier &&
matchesLowStock
);
});
// Сортировка
filtered.sort((a, b) => {
let aValue: any = a[sort.field];
let bValue: any = b[sort.field];
if (typeof aValue === "string") {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
if (sort.direction === "asc") {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
return filtered;
}, [consolidatedSupplies, filters, sort]);
// Группировка
const groupedSupplies = useMemo(() => {
if (groupBy === "none")
return { "Все расходники": filteredAndSortedSupplies };
return filteredAndSortedSupplies.reduce((acc, supply) => {
const key = supply[groupBy] || "Без категории";
if (!acc[key]) acc[key] = [];
acc[key].push(supply);
return acc;
}, {} as Record<string, Supply[]>);
}, [filteredAndSortedSupplies, groupBy]);
// Обработчики
const handleSort = useCallback((field: SortState["field"]) => {
setSort((prev) => ({
field,
direction:
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
}));
}, []);
const toggleSupplyExpansion = useCallback((supplyId: string) => {
setExpandedSupplies((prev) => {
const newSet = new Set(prev);
if (newSet.has(supplyId)) {
newSet.delete(supplyId);
} else {
newSet.add(supplyId);
}
return newSet;
});
}, []);
const handleExport = useCallback(() => {
const csvData = filteredAndSortedSupplies.map((supply) => ({
Название: supply.name,
Описание: supply.description,
Категория: supply.category,
Статус: getStatusConfig(supply.status).label,
"Текущий остаток": supply.currentStock,
"Минимальный остаток": supply.minStock,
Единица: supply.unit,
Цена: supply.price,
Поставщик: supply.supplier,
"Дата создания": new Date(supply.createdAt).toLocaleDateString("ru-RU"),
}));
const csv = [
Object.keys(csvData[0]).join(","),
...csvData.map((row) => Object.values(row).join(",")),
].join("\n");
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = `расходники_фф_${
new Date().toISOString().split("T")[0]
}.csv`;
link.click();
toast.success("Данные экспортированы в CSV");
}, [filteredAndSortedSupplies, getStatusConfig]);
const handleRefresh = useCallback(() => {
refetch();
toast.success("Данные обновлены");
}, [refetch]);
if (loading) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300 overflow-hidden`}
>
<div className="flex-1 overflow-y-auto flex items-center justify-center">
<div className="text-white">Загрузка...</div>
</div>
</main>
</div>
);
}
if (error) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300 overflow-hidden`}
>
<div className="flex-1 overflow-y-auto flex items-center justify-center">
<div className="text-red-300">Ошибка загрузки: {error.message}</div>
</div>
</main>
</div>
);
}
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300 overflow-hidden`}
>
<div className="flex-1 overflow-y-auto space-y-6">
{/* Заголовок и фильтры */}
<SuppliesHeader
viewMode={viewMode}
onViewModeChange={setViewMode}
groupBy={groupBy}
onGroupByChange={setGroupBy}
filters={filters}
onFiltersChange={setFilters}
showFilters={showFilters}
onToggleFilters={() => setShowFilters(!showFilters)}
onExport={handleExport}
onRefresh={handleRefresh}
/>
{/* Статистика */}
<SuppliesStats supplies={consolidatedSupplies} />
{/* Основной контент */}
<div className="space-y-6">
{groupBy === "none" ? (
// Без группировки
<>
{viewMode === "grid" && (
<SuppliesGrid
supplies={filteredAndSortedSupplies}
expandedSupplies={expandedSupplies}
onToggleExpansion={toggleSupplyExpansion}
getSupplyDeliveries={getSupplyDeliveries}
getStatusConfig={getStatusConfig}
/>
)}
{viewMode === "list" && (
<SuppliesList
supplies={filteredAndSortedSupplies}
expandedSupplies={expandedSupplies}
onToggleExpansion={toggleSupplyExpansion}
getSupplyDeliveries={getSupplyDeliveries}
getStatusConfig={getStatusConfig}
sort={sort}
onSort={handleSort}
/>
)}
{viewMode === "analytics" && (
<div className="text-center text-white/60 py-12">
Аналитический режим будет добавлен позже
</div>
)}
</>
) : (
// С группировкой
<div className="space-y-6">
{Object.entries(groupedSupplies).map(
([groupName, groupSupplies]) => (
<div key={groupName} className="space-y-4">
<h3 className="text-lg font-semibold text-white flex items-center space-x-2">
<span>{groupName}</span>
<span className="text-sm text-white/60">
({groupSupplies.length})
</span>
</h3>
{viewMode === "grid" && (
<SuppliesGrid
supplies={groupSupplies}
expandedSupplies={expandedSupplies}
onToggleExpansion={toggleSupplyExpansion}
getSupplyDeliveries={getSupplyDeliveries}
getStatusConfig={getStatusConfig}
/>
)}
{viewMode === "list" && (
<SuppliesList
supplies={groupSupplies}
expandedSupplies={expandedSupplies}
onToggleExpansion={toggleSupplyExpansion}
getSupplyDeliveries={getSupplyDeliveries}
getStatusConfig={getStatusConfig}
sort={sort}
onSort={handleSort}
/>
)}
</div>
)
)}
</div>
)}
</div>
</div>
</main>
</div>
);
}