supply.defectTotal > 0
+ (supply) => supply.status === "delivered"
).length
}
- icon={AlertTriangle}
- iconColor="text-red-400"
- iconBg="bg-red-500/20"
- trend={{ value: 2, isPositive: false }}
- subtitle="Дефектные материалы ФФ"
+ icon={Calendar}
+ iconColor="text-blue-400"
+ iconBg="bg-blue-500/20"
+ trend={{ value: 3, isPositive: true }}
+ subtitle="Завершенные поставки"
/>
@@ -408,7 +408,6 @@ export function FulfillmentDetailedSuppliesTab() {
План |
Факт |
- Брак |
Цена расходников
|
@@ -473,17 +472,6 @@ export function FulfillmentDetailedSuppliesTab() {
{supply.actualTotal}
-
- 0
- ? "text-red-400"
- : "text-white"
- }`}
- >
- {supply.defectTotal}
-
- |
{formatCurrency(supply.totalConsumablesPrice)}
diff --git a/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx b/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx
index 4536406..c547d56 100644
--- a/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx
+++ b/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx
@@ -371,12 +371,8 @@ export function SuppliesConsumablesTab() {
return consumable.actualQty * consumable.unitPrice;
};
- const getEfficiencyBadge = (
- planned: number,
- actual: number,
- defect: number
- ) => {
- const efficiency = ((actual - defect) / planned) * 100;
+ const getEfficiencyBadge = (planned: number, actual: number) => {
+ const efficiency = (actual / planned) * 100;
if (efficiency >= 95) {
return (
@@ -455,15 +451,15 @@ export function SuppliesConsumablesTab() {
-
-
+
+
- С браком
+ Завершено
{
mockConsumableSupplies.filter(
- (supply) => supply.defectTotal > 0
+ (supply) => supply.status === "completed"
).length
}
@@ -487,7 +483,6 @@ export function SuppliesConsumablesTab() {
План |
Факт |
- Брак |
Цена расходников
|
@@ -543,17 +538,6 @@ export function SuppliesConsumablesTab() {
{supply.actualTotal}
|
-
- 0
- ? "text-red-400"
- : "text-white"
- }`}
- >
- {supply.defectTotal}
-
- |
{formatCurrency(supply.totalConsumablesPrice)}
@@ -646,19 +630,6 @@ export function SuppliesConsumablesTab() {
)}
|
-
-
- {route.suppliers.reduce(
- (sum, s) =>
- sum +
- s.consumables.reduce(
- (cSum, c) => cSum + c.defectQty,
- 0
- ),
- 0
- )}
-
- |
{formatCurrency(route.totalConsumablesPrice)}
@@ -741,14 +712,6 @@ export function SuppliesConsumablesTab() {
)}
|
-
-
- {supplier.consumables.reduce(
- (sum, c) => sum + c.defectQty,
- 0
- )}
-
- |
{formatCurrency(
@@ -832,17 +795,6 @@ export function SuppliesConsumablesTab() {
{consumable.actualQty}
|
-
- 0
- ? "text-red-400"
- : "text-white"
- }`}
- >
- {consumable.defectQty}
-
- |
@@ -863,8 +815,7 @@ export function SuppliesConsumablesTab() {
{getEfficiencyBadge(
consumable.plannedQty,
- consumable.actualQty,
- consumable.defectQty
+ consumable.actualQty
)}
|
@@ -883,7 +834,7 @@ export function SuppliesConsumablesTab() {
{isConsumableExpanded && (
|
diff --git a/src/components/supplies/create-consumables-supply-page.tsx b/src/components/supplies/create-consumables-supply-page.tsx
new file mode 100644
index 0000000..9b61ae4
--- /dev/null
+++ b/src/components/supplies/create-consumables-supply-page.tsx
@@ -0,0 +1,645 @@
+"use client";
+
+import React, { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useQuery, useMutation } from "@apollo/client";
+import { Sidebar } from "@/components/dashboard/sidebar";
+import { useSidebar } from "@/hooks/useSidebar";
+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 {
+ ArrowLeft,
+ Building2,
+ MapPin,
+ Phone,
+ Mail,
+ Star,
+ Search,
+ Package,
+ Plus,
+ Minus,
+ ShoppingCart,
+ Wrench,
+ Box,
+} from "lucide-react";
+import { GET_MY_COUNTERPARTIES, GET_ALL_PRODUCTS } from "@/graphql/queries";
+import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations";
+import { OrganizationAvatar } from "@/components/market/organization-avatar";
+import { toast } from "sonner";
+import Image from "next/image";
+
+interface ConsumableSupplier {
+ id: string;
+ inn: string;
+ name?: string;
+ fullName?: string;
+ type: "FULFILLMENT" | "SELLER" | "LOGIST" | "WHOLESALE";
+ address?: string;
+ phones?: Array<{ value: string }>;
+ emails?: Array<{ value: string }>;
+ users?: Array<{ id: string; avatar?: string; managerName?: string }>;
+ createdAt: string;
+}
+
+interface ConsumableProduct {
+ id: string;
+ name: string;
+ description?: string;
+ price: number;
+ category?: { name: string };
+ images?: Array<{ url: string }>;
+ organization: {
+ id: string;
+ name: string;
+ };
+ stock?: number;
+ unit?: string;
+}
+
+interface SelectedConsumable {
+ id: string;
+ name: string;
+ price: number;
+ selectedQuantity: number;
+ unit?: string;
+ category?: string;
+ supplierId: string;
+ supplierName: string;
+}
+
+export function CreateConsumablesSupplyPage() {
+ const router = useRouter();
+ const { getSidebarMargin } = useSidebar();
+ const [selectedSupplier, setSelectedSupplier] =
+ useState (null);
+ const [selectedConsumables, setSelectedConsumables] = useState<
+ SelectedConsumable[]
+ >([]);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [productSearchQuery, setProductSearchQuery] = useState("");
+ const [deliveryDate, setDeliveryDate] = useState("");
+ const [isCreatingSupply, setIsCreatingSupply] = useState(false);
+
+ // Загружаем контрагентов-поставщиков расходников
+ const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(
+ GET_MY_COUNTERPARTIES
+ );
+
+ // Загружаем товары для выбранного поставщика
+ const { data: productsData, loading: productsLoading } = useQuery(
+ GET_ALL_PRODUCTS,
+ {
+ skip: !selectedSupplier,
+ variables: { search: productSearchQuery || null, category: null },
+ }
+ );
+
+ // Мутация для создания заказа поставки расходников
+ const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER);
+
+ // Фильтруем только поставщиков расходников (оптовиков)
+ const consumableSuppliers = (
+ counterpartiesData?.myCounterparties || []
+ ).filter((org: ConsumableSupplier) => org.type === "WHOLESALE");
+
+ // Фильтруем поставщиков по поисковому запросу
+ const filteredSuppliers = consumableSuppliers.filter(
+ (supplier: ConsumableSupplier) =>
+ supplier.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ supplier.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ supplier.inn?.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ // Фильтруем товары по выбранному поставщику
+ const supplierProducts = selectedSupplier
+ ? (productsData?.allProducts || []).filter(
+ (product: ConsumableProduct) =>
+ product.organization.id === selectedSupplier.id
+ )
+ : [];
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat("ru-RU", {
+ style: "currency",
+ currency: "RUB",
+ minimumFractionDigits: 0,
+ }).format(amount);
+ };
+
+ const renderStars = (rating: number = 4.5) => {
+ return Array.from({ length: 5 }, (_, i) => (
+
+ ));
+ };
+
+ const updateConsumableQuantity = (productId: string, quantity: number) => {
+ const product = supplierProducts.find(
+ (p: ConsumableProduct) => p.id === productId
+ );
+ if (!product || !selectedSupplier) return;
+
+ setSelectedConsumables((prev) => {
+ const existing = prev.find((p) => p.id === productId);
+
+ if (quantity === 0) {
+ // Удаляем расходник если количество 0
+ return prev.filter((p) => p.id !== productId);
+ }
+
+ if (existing) {
+ // Обновляем количество существующего расходника
+ return prev.map((p) =>
+ p.id === productId ? { ...p, selectedQuantity: quantity } : p
+ );
+ } else {
+ // Добавляем новый расходник
+ return [
+ ...prev,
+ {
+ id: product.id,
+ name: product.name,
+ price: product.price,
+ selectedQuantity: quantity,
+ unit: product.unit || "шт",
+ category: product.category?.name || "Расходники",
+ supplierId: selectedSupplier.id,
+ supplierName:
+ selectedSupplier.name || selectedSupplier.fullName || "Поставщик",
+ },
+ ];
+ }
+ });
+ };
+
+ const getSelectedQuantity = (productId: string): number => {
+ const selected = selectedConsumables.find((p) => p.id === productId);
+ return selected ? selected.selectedQuantity : 0;
+ };
+
+ const getTotalAmount = () => {
+ return selectedConsumables.reduce(
+ (sum, consumable) => sum + consumable.price * consumable.selectedQuantity,
+ 0
+ );
+ };
+
+ const getTotalItems = () => {
+ return selectedConsumables.reduce(
+ (sum, consumable) => sum + consumable.selectedQuantity,
+ 0
+ );
+ };
+
+ const handleCreateSupply = async () => {
+ if (!selectedSupplier || selectedConsumables.length === 0 || !deliveryDate) {
+ toast.error("Заполните все обязательные поля");
+ return;
+ }
+
+ setIsCreatingSupply(true);
+
+ try {
+ const result = await createSupplyOrder({
+ variables: {
+ input: {
+ partnerId: selectedSupplier.id,
+ deliveryDate: deliveryDate,
+ items: selectedConsumables.map((consumable) => ({
+ productId: consumable.id,
+ quantity: consumable.selectedQuantity,
+ })),
+ },
+ },
+ });
+
+ if (result.data?.createSupplyOrder?.success) {
+ toast.success("Поставка расходников создана успешно!");
+ router.push("/supplies");
+ } else {
+ toast.error(
+ result.data?.createSupplyOrder?.message || "Ошибка при создании поставки"
+ );
+ }
+ } catch (error) {
+ console.error("Error creating consumables supply:", error);
+ toast.error("Ошибка при создании поставки расходников");
+ } finally {
+ setIsCreatingSupply(false);
+ }
+ };
+
+ // Если выбран поставщик, показываем его товары
+ if (selectedSupplier) {
+ return (
+
+
+
+
+ {/* Заголовок с навигацией */}
+
+
+
+
+
+ Расходники поставщика
+
+
+ {selectedSupplier.name || selectedSupplier.fullName}
+
+
+
+
+
+
+ {/* Поиск по товарам */}
+
+
+
+ setProductSearchQuery(e.target.value)}
+ className="bg-white/10 border-white/20 text-white placeholder-white/40 pl-10"
+ />
+
+
+
+ {/* Основной контент */}
+
+ {/* Список товаров */}
+
+ {productsLoading ? (
+
+ Загрузка расходников...
+
+ ) : supplierProducts.length === 0 ? (
+
+
+
+ У данного поставщика нет доступных расходников
+
+
+ ) : (
+
+ {supplierProducts.map((product: ConsumableProduct) => {
+ const selectedQuantity = getSelectedQuantity(product.id);
+ return (
+
+
+ {/* Изображение товара */}
+
+ {product.images && product.images.length > 0 ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* Информация о товаре */}
+
+
+ {product.name}
+
+ {product.category && (
+
+ {product.category.name}
+
+ )}
+
+ {product.description || "Описание отсутствует"}
+
+
+
+ {formatCurrency(product.price)}
+ {product.unit && (
+
+ / {product.unit}
+
+ )}
+
+ {product.stock && (
+
+ В наличии: {product.stock}
+
+ )}
+
+
+
+ {/* Управление количеством */}
+
+
+
+
+ {selectedQuantity}
+
+
+
+ {selectedQuantity > 0 && (
+
+ {formatCurrency(
+ product.price * selectedQuantity
+ )}
+
+ )}
+
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Корзина */}
+ {selectedConsumables.length > 0 && (
+
+
+
+
+ Корзина ({getTotalItems()} шт)
+
+
+
+ {selectedConsumables.map((consumable) => (
+
+
+
+ {consumable.name}
+
+
+ {formatCurrency(consumable.price)} ×{" "}
+ {consumable.selectedQuantity}
+
+
+
+
+ {formatCurrency(
+ consumable.price * consumable.selectedQuantity
+ )}
+
+
+
+
+ ))}
+
+
+
+
+
+ setDeliveryDate(e.target.value)}
+ className="bg-white/10 border-white/20 text-white"
+ min={new Date().toISOString().split('T')[0]}
+ required
+ />
+
+
+ Итого:
+
+ {formatCurrency(getTotalAmount())}
+
+
+
+
+
+
+ )}
+
+
+
+
+ );
+ }
+
+ // Показываем список поставщиков
+ return (
+
+
+
+
+ {/* Заголовок */}
+
+
+
+ Создание поставки расходников
+
+
+ Выберите поставщика расходников для создания поставки
+
+
+
+
+
+ {/* Поиск */}
+
+
+
+ setSearchQuery(e.target.value)}
+ className="bg-white/10 border-white/20 text-white placeholder-white/40 pl-10 max-w-md"
+ />
+
+
+
+ {/* Список поставщиков */}
+
+ {counterpartiesLoading ? (
+
+ Загрузка поставщиков...
+
+ ) : filteredSuppliers.length === 0 ? (
+
+
+
+ {searchQuery
+ ? "Поставщики не найдены"
+ : "У вас пока нет партнеров-поставщиков расходников"}
+
+
+ ) : (
+
+ {filteredSuppliers.map((supplier: ConsumableSupplier) => (
+ setSelectedSupplier(supplier)}
+ >
+
+ {/* Аватар и основная информация */}
+
+
+
+
+ {supplier.name || supplier.fullName || "Поставщик"}
+
+
+ {renderStars()}
+
+ 4.5
+
+
+
+
+ Поставщик расходников
+
+
+
+
+ {/* Контактная информация */}
+
+ {supplier.address && (
+
+
+ {supplier.address}
+
+ )}
+ {supplier.phones && supplier.phones.length > 0 && (
+
+
+ {supplier.phones[0].value}
+
+ )}
+ {supplier.emails && supplier.emails.length > 0 && (
+
+
+
+ {supplier.emails[0].value}
+
+
+ )}
+
+
+ {/* Дополнительная информация */}
+
+
+ ИНН:
+ {supplier.inn}
+
+
+
+ {/* Кнопка выбора */}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx
index cd3c8b6..5a3eb7a 100644
--- a/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx
+++ b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx
@@ -310,17 +310,17 @@ export function FulfillmentSuppliesTab() {
/>
supply.defectTotal > 0
+ (supply) => supply.status === "delivered"
).length
}
- icon={AlertTriangle}
- iconColor="text-red-400"
- iconBg="bg-red-500/20"
- trend={{ value: 2, isPositive: false }}
- subtitle="Дефектные материалы"
+ icon={Calendar}
+ iconColor="text-blue-400"
+ iconBg="bg-blue-500/20"
+ trend={{ value: 3, isPositive: true }}
+ subtitle="Завершенные поставки"
/>
@@ -339,7 +339,6 @@ export function FulfillmentSuppliesTab() {
План |
Факт |
- Брак |
Цена расходников
|
@@ -395,17 +394,6 @@ export function FulfillmentSuppliesTab() {
{supply.actualTotal}
|
-
- 0
- ? "text-red-400"
- : "text-white"
- }`}
- >
- {supply.defectTotal}
-
- |
{formatCurrency(supply.totalConsumablesPrice)}
diff --git a/src/components/supplies/supplies-dashboard.tsx b/src/components/supplies/supplies-dashboard.tsx
index d899a38..eb57c36 100644
--- a/src/components/supplies/supplies-dashboard.tsx
+++ b/src/components/supplies/supplies-dashboard.tsx
@@ -5,9 +5,15 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
-import { Plus } from "lucide-react";
+import { Plus, Package, Wrench, ChevronDown } from "lucide-react";
import { FulfillmentSuppliesTab } from "./fulfillment-supplies/fulfillment-supplies-tab";
import { MarketplaceSuppliesTab } from "./marketplace-supplies/marketplace-supplies-tab";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
export function SuppliesDashboard() {
const { getSidebarMargin } = useSidebar();
@@ -42,15 +48,38 @@ export function SuppliesDashboard() {
-
+
+
+
+
+
+ {
+ window.location.href = "/supplies/create";
+ }}
+ className="text-white hover:bg-white/10 cursor-pointer"
+ >
+
+ Поставка товаров
+
+ {
+ window.location.href = "/supplies/create-consumables";
+ }}
+ className="text-white hover:bg-white/10 cursor-pointer"
+ >
+
+ Поставка расходников
+
+
+
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..995409e
--- /dev/null
+++ b/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,200 @@
+"use client";
+
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
| |