From ef2d207ee489cf8638aad71993a6a86c9bae7149 Mon Sep 17 00:00:00 2001 From: Veronika Smirnova Date: Thu, 24 Jul 2025 12:18:36 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=20dropdown-menu=20=D0=B8=20=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=81=D0=BE=D0=BE=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B.=20=D0=98?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=B3?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=B8=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=B8=20=D0=B2=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=B0=D1=85=20FulfillmentD?= =?UTF-8?q?etailedSuppliesTab=20=D0=B8=20FulfillmentSuppliesTab=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B2=D0=BE=D1=81=D0=BF=D1=80=D0=B8=D1=8F=D1=82=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85.=20=D0=A3=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=89=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=80=D0=B0=D1=81=D1=87=D0=B5=D1=82=D0=B0=20?= =?UTF-8?q?=D1=8D=D1=84=D1=84=D0=B5=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82=D0=B5=20SuppliesConsumablesTab,=20?= =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B5=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 70 ++ package.json | 1 + src/app/supplies/create-consumables/page.tsx | 10 + .../fulfillment-detailed-supplies-tab.tsx | 26 +- .../consumables-supplies-tab.tsx | 65 +- .../create-consumables-supply-page.tsx | 645 ++++++++++++++++++ .../fulfillment-supplies-sub-tab.tsx | 26 +- .../supplies/supplies-dashboard.tsx | 49 +- src/components/ui/dropdown-menu.tsx | 200 ++++++ 9 files changed, 987 insertions(+), 105 deletions(-) create mode 100644 src/app/supplies/create-consumables/page.tsx create mode 100644 src/components/supplies/create-consumables-supply-page.tsx create mode 100644 src/components/ui/dropdown-menu.tsx diff --git a/package-lock.json b/package-lock.json index 91bc00f..2546135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-progress": "^1.1.7", @@ -3033,6 +3034,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", @@ -3114,6 +3144,46 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", diff --git a/package.json b/package.json index adc998e..f717c68 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-progress": "^1.1.7", diff --git a/src/app/supplies/create-consumables/page.tsx b/src/app/supplies/create-consumables/page.tsx new file mode 100644 index 0000000..64d5c83 --- /dev/null +++ b/src/app/supplies/create-consumables/page.tsx @@ -0,0 +1,10 @@ +import { AuthGuard } from "@/components/auth-guard"; +import { CreateConsumablesSupplyPage } from "@/components/supplies/create-consumables-supply-page"; + +export default function CreateConsumablesSupplyPageRoute() { + return ( + + + + ); +} diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx index 7d95d79..da73458 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx @@ -379,17 +379,17 @@ export function FulfillmentDetailedSuppliesTab() { /> 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.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, +};